Compare commits
440 Commits
abdullahwa
...
release/ul
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c78bf34ef | ||
|
|
36827914eb | ||
|
|
cab1a24e10 | ||
|
|
f6b7782d24 | ||
|
|
c7bbe8d0d1 | ||
|
|
20fd7ea13b | ||
|
|
d55d38ec12 | ||
|
|
e77c6ee74a | ||
|
|
8cb30bedd8 | ||
|
|
0ab3f5f669 | ||
|
|
9eaab9c2e5 | ||
|
|
ac28626b3c | ||
|
|
3f90fea26c | ||
|
|
a0466852d6 | ||
|
|
9fad507ada | ||
|
|
73351fa8e8 | ||
|
|
c8528a7874 | ||
|
|
124909ab74 | ||
|
|
77f66f3afb | ||
|
|
102288407f | ||
|
|
dd7c35497e | ||
|
|
8331d37b7f | ||
|
|
1d12506b01 | ||
|
|
c22c4ec5a6 | ||
|
|
69fc4be952 | ||
|
|
9d946eacd8 | ||
|
|
0af0935e86 | ||
|
|
fd33842109 | ||
|
|
13b5f3bc12 | ||
|
|
502ad904ea | ||
|
|
594ae27c0e | ||
|
|
0924cb1ba3 | ||
|
|
90ee5800b4 | ||
|
|
6be87b4a82 | ||
|
|
2137e985b3 | ||
|
|
ca93f890e1 | ||
|
|
6a57622a3c | ||
|
|
3d490c3879 | ||
|
|
1e09c83300 | ||
|
|
b660903836 | ||
|
|
3d2b8416f9 | ||
|
|
4afd07201b | ||
|
|
94ead51915 | ||
|
|
587e3f2647 | ||
|
|
3394656ed2 | ||
|
|
66eda1a58d | ||
|
|
c1ccc8c201 | ||
|
|
4ae65cfcee | ||
|
|
ad7e2035bc | ||
|
|
d5d67dbe14 | ||
|
|
3423e5efea | ||
|
|
f3d30925a8 | ||
|
|
1ec2c3b262 | ||
|
|
260df228fb | ||
|
|
6b740a89c6 | ||
|
|
03f8fdbdc3 | ||
|
|
0b86166a57 | ||
|
|
46eefa7592 | ||
|
|
036b4be854 | ||
|
|
eae0bfdca2 | ||
|
|
05f5903cbc | ||
|
|
725ae950f4 | ||
|
|
38c4f3bad3 | ||
|
|
46acf2a5a4 | ||
|
|
e8aafef127 | ||
|
|
cf451770ed | ||
|
|
d9c3975f26 | ||
|
|
5f42857332 | ||
|
|
f6babc2db9 | ||
|
|
67a053f3e0 | ||
|
|
845ab30af5 | ||
|
|
3244ecf70b | ||
|
|
a1390ebf36 | ||
|
|
89f9d9511f | ||
|
|
bc66c74a33 | ||
|
|
d760be1a53 | ||
|
|
929a34a0f6 | ||
|
|
55fc919c6f | ||
|
|
102a93486e | ||
|
|
981ba84163 | ||
|
|
8aa918bfb9 | ||
|
|
4ca2ab9f4e | ||
|
|
c01f1854ee | ||
|
|
987484d205 | ||
|
|
f86496468e | ||
|
|
e916ba29b9 | ||
|
|
6410ce1d8f | ||
|
|
d0eebfa0ea | ||
|
|
77daf2fbad | ||
|
|
354426037e | ||
|
|
b9af9ed700 | ||
|
|
2342eaae82 | ||
|
|
a1484264fb | ||
|
|
cff4a76b0c | ||
|
|
3e2e8095b4 | ||
|
|
ebd63a13a9 | ||
|
|
fd0d08daa1 | ||
|
|
6ae4c2d68b | ||
|
|
95331d1b10 | ||
|
|
7c63b66d8e | ||
|
|
810f506e52 | ||
|
|
33fd669c8f | ||
|
|
5c5204fb17 | ||
|
|
95db89a9dd | ||
|
|
81a878a658 | ||
|
|
b502de846a | ||
|
|
39f0123820 | ||
|
|
7ee70193c0 | ||
|
|
0e1574dba7 | ||
|
|
0d45ae6599 | ||
|
|
78246cf26b | ||
|
|
ca193563ec | ||
|
|
d0efd35e66 | ||
|
|
375b704eef | ||
|
|
ae121358db | ||
|
|
92b7c58af7 | ||
|
|
a4097fe6fc | ||
|
|
397f688300 | ||
|
|
8bd4b1b9a8 | ||
|
|
54d029c181 | ||
|
|
e6a4636147 | ||
|
|
efb4162926 | ||
|
|
6061232e10 | ||
|
|
ba6b8c8f9b | ||
|
|
9c16ba0075 | ||
|
|
02b987909b | ||
|
|
1bcc54bb05 | ||
|
|
5a5b0b905b | ||
|
|
44ed49c7d2 | ||
|
|
386baa3840 | ||
|
|
f1a56ad6bc | ||
|
|
465bb9f7a0 | ||
|
|
f9b7525d44 | ||
|
|
c7e82295c2 | ||
|
|
e02cf28b54 | ||
|
|
18c51e8e73 | ||
|
|
88b444e796 | ||
|
|
71635b33b6 | ||
|
|
515890d5ef | ||
|
|
c2960e1232 | ||
|
|
17e12f7f87 | ||
|
|
ffc39868e9 | ||
|
|
9ba01af816 | ||
|
|
fc222cc76c | ||
|
|
99a39568de | ||
|
|
9ce3cbfddd | ||
|
|
55a72b3f6e | ||
|
|
597004c82e | ||
|
|
3458b6f410 | ||
|
|
b70b4a796f | ||
|
|
d76945d2f2 | ||
|
|
7ee34f7d9a | ||
|
|
96c619cab8 | ||
|
|
9eda5e588c | ||
|
|
3aff0e03e0 | ||
|
|
852358f243 | ||
|
|
5ffd17db4c | ||
|
|
6f3c9616d6 | ||
|
|
5f4812ed47 | ||
|
|
d694b5c428 | ||
|
|
6b73130e9b | ||
|
|
d608f3947e | ||
|
|
db83838d5e | ||
|
|
004cdf35f1 | ||
|
|
6cb015e49d | ||
|
|
c5ae2c40d7 | ||
|
|
46edd0cb70 | ||
|
|
2c6b5c34a9 | ||
|
|
b000d2e048 | ||
|
|
437fba16fe | ||
|
|
c4038b4085 | ||
|
|
496ff21015 | ||
|
|
23e16ac6e0 | ||
|
|
d8c5762ee2 | ||
|
|
4d2d520234 | ||
|
|
b094722772 | ||
|
|
a9f061f10c | ||
|
|
4eb5d4effc | ||
|
|
925bcce81c | ||
|
|
f44e03c7c8 | ||
|
|
999988dd88 | ||
|
|
7ec1226965 | ||
|
|
5b00275371 | ||
|
|
30c1158775 | ||
|
|
c196b60b39 | ||
|
|
3022a267b1 | ||
|
|
19aacdfaf9 | ||
|
|
530cca7c59 | ||
|
|
e66d77cdd1 | ||
|
|
30333edfa2 | ||
|
|
538d85b5dd | ||
|
|
8066ae58f1 | ||
|
|
8ce7362fd8 | ||
|
|
9c6f247f0a | ||
|
|
085bc19313 | ||
|
|
ee7c769c2f | ||
|
|
a37be2dac1 | ||
|
|
d0fdc2ac5d | ||
|
|
8a4eecf6c2 | ||
|
|
e2e4003b44 | ||
|
|
434f114220 | ||
|
|
c31e1648bf | ||
|
|
379d2df2fc | ||
|
|
5933ae3034 | ||
|
|
d1269197ac | ||
|
|
12438e79f5 | ||
|
|
57317a4bb6 | ||
|
|
3bbc068a73 | ||
|
|
4179aa798a | ||
|
|
a1877bb499 | ||
|
|
cd44f15634 | ||
|
|
83242b1042 | ||
|
|
116c30663b | ||
|
|
0ed92cc7e8 | ||
|
|
3ec887089d | ||
|
|
b46165ede0 | ||
|
|
752d760030 | ||
|
|
3e2c4dc760 | ||
|
|
4136a63e92 | ||
|
|
ca8c9ef4fe | ||
|
|
14f1cfd0e1 | ||
|
|
f81ec9544a | ||
|
|
11d7d4beb4 | ||
|
|
70fc2f9642 | ||
|
|
f1d3c88f76 | ||
|
|
c5c05c86f7 | ||
|
|
b845b34b02 | ||
|
|
27c666cc27 | ||
|
|
dcdd024809 | ||
|
|
dab4b1dc92 | ||
|
|
c40bcd49ea | ||
|
|
8e3de876d0 | ||
|
|
aff083335c | ||
|
|
18ea60b470 | ||
|
|
9260b4834c | ||
|
|
d7a5b222ee | ||
|
|
17ce32b80c | ||
|
|
d4693a2c2e | ||
|
|
a446d5d3fc | ||
|
|
8f4139ad87 | ||
|
|
87e591cd71 | ||
|
|
90c02279e3 | ||
|
|
600a3de7b6 | ||
|
|
4390ca5435 | ||
|
|
5903ad2ae8 | ||
|
|
cc50ad9744 | ||
|
|
4e69301a4e | ||
|
|
3a877eb55c | ||
|
|
673639caee | ||
|
|
474f7c018d | ||
|
|
46398e7edc | ||
|
|
dfb06268ab | ||
|
|
b0301d5028 | ||
|
|
89d848e408 | ||
|
|
56ba73e455 | ||
|
|
c3d30227a3 | ||
|
|
f5bedd7708 | ||
|
|
218c796500 | ||
|
|
604845f4a3 | ||
|
|
751cdb1076 | ||
|
|
0ebc4eb3cd | ||
|
|
8a954c19e2 | ||
|
|
18b999b9a8 | ||
|
|
b155d04814 | ||
|
|
6f715e598f | ||
|
|
ab8d6e7913 | ||
|
|
d2bb164fab | ||
|
|
5aacaf44a3 | ||
|
|
4502dbe413 | ||
|
|
f5e2fa7448 | ||
|
|
5954b3122f | ||
|
|
2bf71cce10 | ||
|
|
8fd0320c88 | ||
|
|
da15a70942 | ||
|
|
52f4c51362 | ||
|
|
a0c2fb2686 | ||
|
|
223f792e2b | ||
|
|
175d9cf5cb | ||
|
|
94ef29938f | ||
|
|
4e38c9e9a6 | ||
|
|
81a1ec7e1e | ||
|
|
90da11782f | ||
|
|
82c9e07f0f | ||
|
|
16fb3a1bb4 | ||
|
|
e623f03c4b | ||
|
|
b2173afb9c | ||
|
|
c787a77f9e | ||
|
|
075587c1f0 | ||
|
|
a0b0b1f8d4 | ||
|
|
16f20cad66 | ||
|
|
40c67995a4 | ||
|
|
0fc68f7d93 | ||
|
|
90d3668128 | ||
|
|
1440c8239c | ||
|
|
bb29f9624e | ||
|
|
746b0fed4b | ||
|
|
02d14b95a7 | ||
|
|
484fc95ea0 | ||
|
|
bdb851eb8b | ||
|
|
b984296550 | ||
|
|
39fca96523 | ||
|
|
d1e817d4ba | ||
|
|
b35042ca97 | ||
|
|
88e9eb3fdf | ||
|
|
4409be4cc6 | ||
|
|
e314de2042 | ||
|
|
fc329fe9c2 | ||
|
|
aea3a7c830 | ||
|
|
35b5459dec | ||
|
|
2b82ac9faf | ||
|
|
24ad8851e6 | ||
|
|
b62645349e | ||
|
|
d2c160b771 | ||
|
|
6e39072334 | ||
|
|
245bf9aa6b | ||
|
|
b8a4163629 | ||
|
|
a5730b625a | ||
|
|
f03ac8b6d9 | ||
|
|
9e72f1c9e9 | ||
|
|
9693d938c6 | ||
|
|
f45bb43064 | ||
|
|
32f9804e4a | ||
|
|
feef40f6d0 | ||
|
|
a266e3dca9 | ||
|
|
6cc50a983e | ||
|
|
16caae04af | ||
|
|
2452d09913 | ||
|
|
b7f2a1f689 | ||
|
|
2416782eca | ||
|
|
5d0a428bdf | ||
|
|
fe4909ce07 | ||
|
|
9d61e836a5 | ||
|
|
891e28ec59 | ||
|
|
5ec4893f47 | ||
|
|
c5c0cf5392 | ||
|
|
350c2080ec | ||
|
|
44b718ad02 | ||
|
|
97333ada47 | ||
|
|
3467534bc7 | ||
|
|
5f43f945bb | ||
|
|
0ee0f41f3e | ||
|
|
35b66ae38b | ||
|
|
361e14c980 | ||
|
|
8e967fa3bf | ||
|
|
67feee5e0b | ||
|
|
837ac4e635 | ||
|
|
5c6ddd2888 | ||
|
|
7ef1b5b92d | ||
|
|
672d39f99c | ||
|
|
08a0d8e30b | ||
|
|
208c7a1ada | ||
|
|
afe2a754bd | ||
|
|
594ffe4aa9 | ||
|
|
8ecdedcdc8 | ||
|
|
11e144dec0 | ||
|
|
c71e586e64 | ||
|
|
d0d2aeed71 | ||
|
|
737833cdeb | ||
|
|
c00ebc9f64 | ||
|
|
41ee7e9538 | ||
|
|
b7181d5643 | ||
|
|
50e0235a6a | ||
|
|
5166546048 | ||
|
|
49d3acd1a1 | ||
|
|
cdf799d515 | ||
|
|
ed26467f0d | ||
|
|
fea3991915 | ||
|
|
30ca5f577e | ||
|
|
4047c3c923 | ||
|
|
3d32c8624d | ||
|
|
0c70e31655 | ||
|
|
10e1a451f7 | ||
|
|
7f2d3700d7 | ||
|
|
eb88d9b46d | ||
|
|
26dbac709f | ||
|
|
d6592a77f9 | ||
|
|
91d0feaed9 | ||
|
|
237599c830 | ||
|
|
56d0ad3db9 | ||
|
|
c0b504763e | ||
|
|
4aa44b89a3 | ||
|
|
be661b8a27 | ||
|
|
3c75052d8b | ||
|
|
229a674469 | ||
|
|
2c75c7f790 | ||
|
|
d595986d0e | ||
|
|
43d8784014 | ||
|
|
f15b71d20c | ||
|
|
aae79c45bb | ||
|
|
5233c6aa59 | ||
|
|
d59c9e38bd | ||
|
|
eb35897bf7 | ||
|
|
c28b8c6840 | ||
|
|
1698720aad | ||
|
|
57a2c7bcb6 | ||
|
|
f6f67a1a80 | ||
|
|
aec7e1281f | ||
|
|
cdf0ee5e4e | ||
|
|
f0858b7381 | ||
|
|
5f49bedaa8 | ||
|
|
a94f472459 | ||
|
|
2fc2d86a44 | ||
|
|
899f88ca1e | ||
|
|
399dd04df6 | ||
|
|
46dd8739ab | ||
|
|
5de18e0dba | ||
|
|
ca3bc9151c | ||
|
|
9cccf09394 | ||
|
|
ced29278d5 | ||
|
|
bc8197a9af | ||
|
|
f86677171f | ||
|
|
966b59b70f | ||
|
|
0e6aff7c6f | ||
|
|
13dbb732e3 | ||
|
|
b80748aa4c | ||
|
|
f098fe1a3a | ||
|
|
a989fabb92 | ||
|
|
1257e81781 | ||
|
|
b08890b794 | ||
|
|
7f1c7b86ef | ||
|
|
a5fd3a7f7e | ||
|
|
4513cc8834 | ||
|
|
40103a2386 | ||
|
|
b6c18bb439 | ||
|
|
ee51939f2d | ||
|
|
fc127ccd98 | ||
|
|
22faebac50 | ||
|
|
5ce3995f5b | ||
|
|
e181269703 | ||
|
|
5d212ec6b5 | ||
|
|
363096c4f0 | ||
|
|
85c5902559 | ||
|
|
fca6da2df7 | ||
|
|
7a74e5d29b | ||
|
|
4b94344dd9 | ||
|
|
5245254de5 | ||
|
|
afda76ff11 | ||
|
|
da52ddd35a | ||
|
|
bb2aef3878 | ||
|
|
4d6f76d9b3 |
12
.env
12
.env
@@ -1,12 +1,10 @@
|
||||
ACCESS_TOKEN_COOKIE_NAME=''
|
||||
ACCOUNT_PROFILE_URL=''
|
||||
BASE_URL=''
|
||||
COACHING_ENABLED=''
|
||||
CREDENTIALS_BASE_URL=''
|
||||
CSRF_TOKEN_API_PATH=''
|
||||
DEMOGRAPHICS_BASE_URL=''
|
||||
DISCOVERY_API_BASE_URL=''
|
||||
ECOMMERCE_BASE_URL=''
|
||||
ENABLE_DEMOGRAPHICS_COLLECTION=''
|
||||
FAVICON_URL=''
|
||||
LANGUAGE_PREFERENCE_COOKIE_NAME=''
|
||||
LMS_BASE_URL=''
|
||||
@@ -14,9 +12,11 @@ LOGIN_URL=''
|
||||
LOGO_TRADEMARK_URL=''
|
||||
LOGO_URL=''
|
||||
LOGO_WHITE_URL=''
|
||||
SHOW_PUSH_CHANNEL=''
|
||||
SHOW_EMAIL_CHANNEL=''
|
||||
LOGOUT_URL=''
|
||||
MARKETING_SITE_BASE_URL=''
|
||||
NODE_ENV=''
|
||||
NODE_ENV='production'
|
||||
ORDER_HISTORY_URL=''
|
||||
PUBLISHER_BASE_URL=''
|
||||
REFRESH_ACCESS_TOKEN_ENDPOINT=''
|
||||
@@ -33,3 +33,7 @@ APP_ID=
|
||||
MFE_CONFIG_API_URL=
|
||||
PASSWORD_RESET_SUPPORT_LINK=''
|
||||
LEARNER_FEEDBACK_URL=''
|
||||
SUPPORT_URL_TO_UNLINK_SOCIAL_MEDIA_ACCOUNT='https://help.edx.org/edxlearner/s/article/How-do-I-link-or-unlink-my-edX-account-to-a-social-media-account'
|
||||
COUNTRIES_WITH_DELETE_ACCOUNT_DISABLED='[]'
|
||||
# Fallback in local style files
|
||||
PARAGON_THEME_URLS={}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
|
||||
ACCOUNT_PROFILE_URL='http://localhost:1995'
|
||||
BASE_URL='localhost:1997'
|
||||
COACHING_ENABLED=''
|
||||
CREDENTIALS_BASE_URL='http://localhost:18150'
|
||||
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
|
||||
DEMOGRAPHICS_BASE_URL='http://localhost:18360'
|
||||
DISCOVERY_API_BASE_URL=''
|
||||
ECOMMERCE_BASE_URL='http://localhost:18130'
|
||||
ENABLE_DEMOGRAPHICS_COLLECTION=''
|
||||
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
|
||||
LANGUAGE_PREFERENCE_COOKIE_NAME='openedx-language-preference'
|
||||
LMS_BASE_URL='http://localhost:18000'
|
||||
@@ -30,7 +28,13 @@ ENABLE_COPPA_COMPLIANCE=''
|
||||
ENABLE_ACCOUNT_DELETION=''
|
||||
ENABLE_DOB_UPDATE=''
|
||||
MARKETING_EMAILS_OPT_IN=''
|
||||
SHOW_PUSH_CHANNEL='true'
|
||||
SHOW_EMAIL_CHANNEL='true'
|
||||
APP_ID=
|
||||
MFE_CONFIG_API_URL=
|
||||
PASSWORD_RESET_SUPPORT_LINK='mailto:support@example.com'
|
||||
LEARNER_FEEDBACK_URL=''
|
||||
SUPPORT_URL_TO_UNLINK_SOCIAL_MEDIA_ACCOUNT='https://help.edx.org/edxlearner/s/article/How-do-I-link-or-unlink-my-edX-account-to-a-social-media-account'
|
||||
COUNTRIES_WITH_DELETE_ACCOUNT_DISABLED='[]'
|
||||
# Fallback in local style files
|
||||
PARAGON_THEME_URLS={}
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
|
||||
BASE_URL='localhost:1997'
|
||||
COACHING_ENABLED=''
|
||||
CREDENTIALS_BASE_URL='http://localhost:18150'
|
||||
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
|
||||
DEMOGRAPHICS_BASE_URL='http://localhost:18360'
|
||||
DISCOVERY_API_BASE_URL=''
|
||||
ECOMMERCE_BASE_URL='http://localhost:18130'
|
||||
ENABLE_DEMOGRAPHICS_COLLECTION=''
|
||||
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
|
||||
LANGUAGE_PREFERENCE_COOKIE_NAME='openedx-language-preference'
|
||||
LMS_BASE_URL='http://localhost:18000'
|
||||
@@ -27,8 +24,13 @@ SUPPORT_URL='http://localhost:18000/support'
|
||||
USER_INFO_COOKIE_NAME='edx-user-info'
|
||||
ENABLE_COPPA_COMPLIANCE=''
|
||||
ENABLE_ACCOUNT_DELETION=''
|
||||
SHOW_PUSH_CHANNEL=''
|
||||
SHOW_EMAIL_CHANNEL=''
|
||||
ENABLE_DOB_UPDATE=''
|
||||
MARKETING_EMAILS_OPT_IN=''
|
||||
APP_ID=
|
||||
MFE_CONFIG_API_URL=
|
||||
LEARNER_FEEDBACK_URL=''
|
||||
SUPPORT_URL_TO_UNLINK_SOCIAL_MEDIA_ACCOUNT='https://help.edx.org/edxlearner/s/article/How-do-I-link-or-unlink-my-edX-account-to-a-social-media-account'
|
||||
COUNTRIES_WITH_DELETE_ACCOUNT_DISABLED='[]'
|
||||
PARAGON_THEME_URLS={}
|
||||
|
||||
@@ -3,3 +3,4 @@ dist/
|
||||
node_modules/
|
||||
__mocks__/
|
||||
__snapshots__/
|
||||
src/i18n/messages/
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
const { createConfig } = require('@edx/frontend-build');
|
||||
const { createConfig } = require('@openedx/frontend-build');
|
||||
|
||||
module.exports = createConfig('eslint');
|
||||
|
||||
24
.github/pull_request_template.md
vendored
Normal file
24
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
### Description
|
||||
|
||||
Include a description of your changes here, along with a link to any relevant Jira tickets and/or GitHub issues.
|
||||
|
||||
#### How Has This Been Tested?
|
||||
|
||||
Please describe in detail how you tested your changes.
|
||||
|
||||
#### Screenshots/sandbox (optional):
|
||||
Include a link to the sandbox for design changes or screenshot for before and after. **Remove this section if it's not applicable.**
|
||||
|
||||
|Before|After|
|
||||
|-------|-----|
|
||||
| | |
|
||||
|
||||
#### Merge Checklist
|
||||
|
||||
* [ ] If your update includes visual changes, have they been reviewed by a designer? Send them a link to the Sandbox, if applicable.
|
||||
* [ ] Is there adequate test coverage for your changes?
|
||||
|
||||
#### Post-merge Checklist
|
||||
|
||||
* [ ] Deploy the changes to prod after verifying on stage or ask **@openedx/edx-infinity** to do it.
|
||||
* [ ] 🎉 🙌 Celebrate! Thanks for your contribution.
|
||||
16
.github/workflows/ci.yml
vendored
16
.github/workflows/ci.yml
vendored
@@ -14,16 +14,16 @@ jobs:
|
||||
- lint
|
||||
- test
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Setup Nodejs Env
|
||||
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
|
||||
- uses: actions/setup-node@v2
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VER }}
|
||||
node-version-file: '.nvmrc'
|
||||
- run: make requirements
|
||||
- run: make test NPM_TESTS=build
|
||||
- run: make test NPM_TESTS=${{ matrix.npm-test }}
|
||||
- name: upload coverage
|
||||
uses: codecov/codecov-action@v3
|
||||
- name: Coverage
|
||||
if: matrix.npm-test == 'test'
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
fail_ci_if_error: false
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
fail_ci_if_error: true
|
||||
|
||||
3
.github/workflows/lockfileversion-check.yml
vendored
3
.github/workflows/lockfileversion-check.yml
vendored
@@ -10,5 +10,4 @@ on:
|
||||
|
||||
jobs:
|
||||
version-check:
|
||||
uses: openedx/.github/.github/workflows/lockfile-check.yml@master
|
||||
|
||||
uses: openedx/.github/.github/workflows/lockfileversion-check-v3.yml@master
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -17,3 +17,4 @@ temp/babel-plugin-react-intl
|
||||
/temp
|
||||
/.vscode
|
||||
/module.config.js
|
||||
src/i18n/messages/
|
||||
@@ -1,9 +0,0 @@
|
||||
[main]
|
||||
host = https://www.transifex.com
|
||||
|
||||
[o:open-edx:p:edx-platform:r:frontend-app-account]
|
||||
file_filter = src/i18n/messages/<lang>.json
|
||||
source_file = src/i18n/transifex_input.json
|
||||
source_lang = en
|
||||
type = KEYVALUEJSON
|
||||
|
||||
34
Makefile
34
Makefile
@@ -1,14 +1,10 @@
|
||||
export TRANSIFEX_RESOURCE = frontend-app-account
|
||||
transifex_resource = frontend-app-account
|
||||
transifex_langs = "ar,de,es_419,fa_IR,fr,fr_CA,hi,it,pt,ru,uk,zh_CN,it_IT,pt_PT,de_DE"
|
||||
|
||||
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
|
||||
|
||||
# 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
|
||||
|
||||
@@ -21,6 +17,11 @@ test.npm.%: validate-no-uncommitted-package-lock-changes
|
||||
npm run $(*)
|
||||
|
||||
.PHONY: requirements
|
||||
|
||||
precommit:
|
||||
npm run lint
|
||||
npm audit
|
||||
|
||||
requirements: ## install ci requirements
|
||||
npm ci
|
||||
|
||||
@@ -40,35 +41,18 @@ detect_changed_source_translations:
|
||||
# Checking for changed translations...
|
||||
git diff --exit-code $(i18n)
|
||||
|
||||
# Pushes translations to Transifex. You must run make extract_translations first.
|
||||
push_translations:
|
||||
# Pushing strings to Transifex...
|
||||
tx push -s
|
||||
# Fetching hashes from Transifex...
|
||||
./node_modules/@edx/reactifex/bash_scripts/get_hashed_strings_v3.sh
|
||||
# Writing out comments to file...
|
||||
$(transifex_utils) $(transifex_temp) --comments --v3-scripts-path
|
||||
# Pushing comments to Transifex...
|
||||
./node_modules/@edx/reactifex/bash_scripts/put_comments_v3.sh
|
||||
|
||||
ifeq ($(OPENEDX_ATLAS_PULL),)
|
||||
# Pulls translations from Transifex.
|
||||
pull_translations:
|
||||
tx pull -t -f --mode reviewed --languages=$(transifex_langs)
|
||||
else
|
||||
# Experimental: OEP-58 Pulls translations using atlas
|
||||
pull_translations:
|
||||
rm -rf src/i18n/messages
|
||||
mkdir src/i18n/messages
|
||||
cd src/i18n/messages \
|
||||
&& atlas pull --filter=$(transifex_langs) \
|
||||
&& atlas pull $(ATLAS_OPTIONS) \
|
||||
translations/frontend-platform/src/i18n/messages:frontend-platform \
|
||||
translations/paragon/src/i18n/messages:paragon \
|
||||
translations/frontend-component-footer/src/i18n/messages:frontend-component-footer \
|
||||
translations/frontend-component-header/src/i18n/messages:frontend-component-header \
|
||||
translations/frontend-app-account/src/i18n/messages:frontend-app-account
|
||||
|
||||
$(intl_imports) paragon frontend-component-header frontend-component-footer frontend-app-account
|
||||
endif
|
||||
$(intl_imports) frontend-platform paragon frontend-component-header frontend-component-footer frontend-app-account
|
||||
|
||||
# This target is used by Travis.
|
||||
validate-no-uncommitted-package-lock-changes:
|
||||
|
||||
203
README.rst
203
README.rst
@@ -1,10 +1,13 @@
|
||||
####################
|
||||
frontend-app-account
|
||||
####################
|
||||
|
||||
|ci-badge| |Codecov| |npm_version| |npm_downloads| |license| |semantic-release|
|
||||
|
||||
frontend-app-account
|
||||
====================
|
||||
|
||||
Introduction
|
||||
------------
|
||||
********
|
||||
Purpose
|
||||
********
|
||||
|
||||
This is a micro-frontend application responsible for the display and updating of a user's account information.
|
||||
|
||||
@@ -13,37 +16,33 @@ What is the domain of this MFE?
|
||||
In this MFE: Private user settings UIs. Public facing profile is in a `separate MFE (Profile) <https://github.com/openedx/frontend-app-profile>`_
|
||||
|
||||
- Account settings page
|
||||
|
||||
- Demographics collection
|
||||
|
||||
- IDV (Identity Verification)
|
||||
|
||||
Installation
|
||||
------------
|
||||
***************
|
||||
Getting Started
|
||||
***************
|
||||
|
||||
This MFE is bundled with `Devstack <https://github.com/openedx/devstack>`_, see the `Getting Started <https://github.com/openedx/devstack#getting-started>`_ section for setup instructions.
|
||||
Prerequisites
|
||||
=============
|
||||
|
||||
1. Install Devstack using the `Getting Started <https://github.com/openedx/devstack#getting-started>`_ instructions.
|
||||
`Tutor`_ is currently recommended as a development environment for your
|
||||
new MFE. Please refer
|
||||
to the `relevant tutor-mfe documentation`_ to get started using it.
|
||||
|
||||
2. Start up Devstack, if it's not already started.
|
||||
.. _Tutor: https://github.com/overhangio/tutor
|
||||
|
||||
3. Log in to Devstack (http://localhost:18000/login )
|
||||
.. _relevant tutor-mfe documentation: https://github.com/overhangio/tutor-mfe?tab=readme-ov-file#mfe-development
|
||||
|
||||
4. Within this project, install requirements and start the development server:
|
||||
Plugins
|
||||
=======
|
||||
This MFE can be customized using `Frontend Plugin Framework <https://github.com/openedx/frontend-plugin-framework>`_.
|
||||
|
||||
.. code-block::
|
||||
|
||||
npm install
|
||||
npm start # The server will run on port 1997
|
||||
|
||||
5. Once the dev server is up, visit http://localhost:1997 to access the MFE
|
||||
|
||||
.. image:: ./docs/images/localhost_preview.png
|
||||
The parts of this MFE that can be customized in that manner are documented `here </src/plugin-slots>`_.
|
||||
|
||||
Environment Variables/Setup Notes
|
||||
---------------------------------
|
||||
=================================
|
||||
|
||||
This MFE is configured via environment variables supplied at build time. All micro-frontends have a shared set of required environment variables, as documented in the Open edX Developer Guide under `Required Environment Variables <https://edx.readthedocs.io/projects/edx-developer-docs/en/latest/developers_guide/micro_frontends_in_open_edx.html#required-environment-variables>`__.
|
||||
This MFE is configured via the ``frontend-platform`` configuration module. For more information on MFE configuration see the `Configuration documentation`_.
|
||||
|
||||
The account settings micro-frontend also supports the following additional variable:
|
||||
|
||||
@@ -70,50 +69,148 @@ Example: ``'false'`` | ``''`` (empty strings are true)
|
||||
Enable the account deletion option, defaults to true.
|
||||
To disable account deletion set ``ENABLE_ACCOUNT_DELETION`` to ``'false'`` (string), otherwise it will default to true.
|
||||
|
||||
edX-specific Environment Variables
|
||||
**********************************
|
||||
|
||||
Furthermore, there are several edX-specific environment variables that enable integrations with closed-source services private to the edX organization, and are unsupported in Open edX. Enabling these environment variables will result in undefined behavior in Open edX installations:
|
||||
|
||||
``COACHING_ENABLED``
|
||||
|
||||
Example: ``true`` | ``''`` (empty strings are falsy)
|
||||
|
||||
Enables support for a section of the micro-frontend that helps users arrange for coaching sessions. Integrates with a private coaching plugin and is only used by edx.org.
|
||||
|
||||
``ENABLE_DEMOGRAPHICS_COLLECTION``
|
||||
|
||||
Example: ``true`` | ``''`` (empty strings are falsy)
|
||||
|
||||
Enables support for a section of the account settings page where a user can enter demographics information. Integrates with a private demographics service and is only used by edx.org.
|
||||
|
||||
``DEMOGRAPHICS_BASE_URL``
|
||||
|
||||
Example: ``https://demographics.example.com``
|
||||
|
||||
Required only if ``ENABLE_DEMOGRAPHICS_COLLECTION`` is true. The fully-qualified URL to the private demographics service in the target environment.
|
||||
|
||||
Example build syntax with a single environment variable:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
NODE_ENV=development ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload' npm run build
|
||||
|
||||
For more information see the document: `Micro-frontend applications in Open
|
||||
edX <https://edx.readthedocs.io/projects/edx-developer-docs/en/latest/developers_guide/micro_frontends_in_open_edx.html#required-environment-variables>`__.
|
||||
For more information see the document: `Configuration documentation`_
|
||||
|
||||
.. _Configuration documentation: https://openedx.github.io/frontend-platform/module-Config.html
|
||||
|
||||
Cloning and Startup
|
||||
===================
|
||||
|
||||
.. code-block::
|
||||
|
||||
|
||||
1. Clone your new repo:
|
||||
|
||||
``git clone https://github.com/openedx/frontend-app-account.git``
|
||||
|
||||
2. Use node v18.x.
|
||||
|
||||
The current version of the micro-frontend build scripts support node 18.
|
||||
Using other major versions of node *may* work, but this is unsupported. For
|
||||
convenience, this repository includes an .nvmrc file to help in setting the
|
||||
correct node version via `nvm <https://github.com/nvm-sh/nvm>`_.
|
||||
|
||||
3. Install npm dependencies:
|
||||
|
||||
``cd frontend-app-account && npm ci``
|
||||
|
||||
4. Start the dev server:
|
||||
|
||||
``npm start``
|
||||
|
||||
Or for local development with custom configuration:
|
||||
|
||||
``npm run dev``
|
||||
|
||||
This runs the dev server with PUBLIC_PATH=/account/, MFE_CONFIG_API_URL pointing to localhost:8000, and hosts on apps.local.openedx.io.
|
||||
|
||||
Local module development
|
||||
=========================
|
||||
|
||||
To develop locally on modules that are installed into this app, you'll need to create a ``module.config.js``
|
||||
file (which is git-ignored) that defines where to find your local modules, for instance:
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
module.exports = {
|
||||
/*
|
||||
Modules you want to use from local source code. Adding a module here means that when this app
|
||||
runs its build, it'll resolve the source from peer directories of this app.
|
||||
|
||||
moduleName: the name you use to import code from the module.
|
||||
dir: The relative path to the module's source code.
|
||||
dist: The sub-directory of the source code where it puts its build artifact. Often "dist", though you
|
||||
may want to use "src" if the module installs React as a peer/dev dependency.
|
||||
*/
|
||||
localModules: [
|
||||
{ moduleName: '@openedx/paragon/scss', dir: '../paragon', dist: 'scss' },
|
||||
{ moduleName: '@openedx/paragon', dir: '../paragon', dist: 'dist' },
|
||||
{ moduleName: '@openedx/frontend-enterprise', dir: '../frontend-enterprise', dist: 'src' },
|
||||
{ moduleName: '@openedx/frontend-platform', dir: '../frontend-platform', dist: 'dist' },
|
||||
],
|
||||
};
|
||||
|
||||
See https://github.com/openedx/frontend-build#local-module-configuration-for-webpack for more details.
|
||||
|
||||
Known Issues
|
||||
------------
|
||||
===========
|
||||
|
||||
None
|
||||
|
||||
Development Roadmap
|
||||
-------------------
|
||||
===================
|
||||
|
||||
We don't have anything planned for the core of the MFE (the account settings page) - this MFE is currently in maintenance mode.
|
||||
There may be a replacement for IDV coming down the pipe, so that may be DEPRed.
|
||||
In the future, it's possible that demographics could be modeled as a plugin rather than being hard-coded into this MFE.
|
||||
|
||||
License
|
||||
=======
|
||||
|
||||
The code in this repository is licensed under the AGPLv3 unless otherwise
|
||||
noted.
|
||||
|
||||
Please see `LICENSE <LICENSE>`_ for details.
|
||||
|
||||
Contributing
|
||||
============
|
||||
|
||||
Contributions are very welcome. Please read `How To Contribute`_ for details.
|
||||
|
||||
.. _How To Contribute: https://openedx.org/r/how-to-contribute
|
||||
|
||||
This project is currently accepting all types of contributions, bug fixes,
|
||||
security fixes, maintenance work, or new features. However, please make sure
|
||||
to have a discussion about your new feature idea with the maintainers prior to
|
||||
beginning development to maximize the chances of your change being accepted.
|
||||
You can start a conversation by creating a new issue on this repo summarizing
|
||||
your idea.
|
||||
|
||||
|
||||
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-account/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
|
||||
|
||||
|
||||
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/
|
||||
|
||||
People
|
||||
======
|
||||
The assigned maintainers for this component and other project details may be found in Backstage or from inspecting catalog-info.yaml.
|
||||
|
||||
Reporting Security Issues
|
||||
=========================
|
||||
|
||||
Please do not report security issues in public. Please email security@openedx.org.
|
||||
|
||||
==============================
|
||||
|
||||
@@ -121,7 +218,7 @@ In the future, it's possible that demographics could be modeled as a plugin rath
|
||||
:target: https://github.com/openedx/edx-developer-docs/actions/workflows/ci.yml
|
||||
:alt: Continuous Integration
|
||||
.. |Codecov| image:: https://img.shields.io/codecov/c/github/edx/frontend-app-account
|
||||
:target: https://codecov.io/gh/edx/frontend-app-account
|
||||
:target: https://codecov.io/gh/openedx/frontend-app-account/
|
||||
.. |npm_version| image:: https://img.shields.io/npm/v/@edx/frontend-app-account.svg
|
||||
:target: @edx/frontend-app-account
|
||||
.. |npm_downloads| image:: https://img.shields.io/npm/dt/@edx/frontend-app-account.svg
|
||||
|
||||
19
catalog-info.yaml
Normal file
19
catalog-info.yaml
Normal file
@@ -0,0 +1,19 @@
|
||||
# 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-account'
|
||||
description: "Open edX micro-frontend application for managing user account information."
|
||||
links:
|
||||
- url: "https://github.com/openedx/frontend-app-account"
|
||||
title: "Frontend app account"
|
||||
icon: "Web"
|
||||
annotations:
|
||||
openedx.org/arch-interest-groups: ""
|
||||
openedx.org/release: "master"
|
||||
spec:
|
||||
owner: group:2u-infinity
|
||||
type: 'website'
|
||||
lifecycle: 'production'
|
||||
15
codecov.yml
Normal file
15
codecov.yml
Normal file
@@ -0,0 +1,15 @@
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
enabled: yes
|
||||
target: auto
|
||||
threshold: 0%
|
||||
patch:
|
||||
default:
|
||||
enabled: yes
|
||||
target: auto
|
||||
threshold: 0%
|
||||
ignore:
|
||||
- "src/i18n"
|
||||
- "src/index.jsx"
|
||||
@@ -1,4 +1,4 @@
|
||||
const { createConfig } = require('@edx/frontend-build');
|
||||
const { createConfig } = require('@openedx/frontend-build');
|
||||
|
||||
module.exports = createConfig('jest', {
|
||||
setupFilesAfterEnv: [
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
# This file describes this Open edX repo, as described in OEP-2:
|
||||
# https://open-edx-proposals.readthedocs.io/en/latest/oep-0002-bp-repo-metadata.html#specification
|
||||
|
||||
nick: acct
|
||||
oeps: {}
|
||||
openedx-release: {ref: master}
|
||||
28928
package-lock.json
generated
28928
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
78
package.json
78
package.json
@@ -10,8 +10,10 @@
|
||||
},
|
||||
"scripts": {
|
||||
"build": "fedx-scripts webpack",
|
||||
"i18n_extract": "BABEL_ENV=i18n fedx-scripts babel src --quiet > /dev/null",
|
||||
"dev": "PUBLIC_PATH=/account/ MFE_CONFIG_API_URL='http://localhost:8000/api/mfe_config/v1' fedx-scripts webpack-dev-server --progress --host apps.local.openedx.io",
|
||||
"i18n_extract": "fedx-scripts formatjs extract",
|
||||
"lint": "fedx-scripts eslint --ext .js --ext .jsx .",
|
||||
"lint:fix": "npm run lint -- --fix",
|
||||
"snapshot": "fedx-scripts jest --updateSnapshot",
|
||||
"start": "fedx-scripts webpack-dev-server --progress",
|
||||
"test": "TZ=UTC fedx-scripts jest --coverage --passWithNoTests"
|
||||
@@ -27,24 +29,26 @@
|
||||
"extends @edx/browserslist-config"
|
||||
],
|
||||
"dependencies": {
|
||||
"@edx/brand": "npm:@edx/brand-openedx@1.2.0",
|
||||
"@edx/frontend-component-footer": "12.2.1",
|
||||
"@edx/frontend-component-header": "4.6.0",
|
||||
"@edx/frontend-platform": "5.1.0",
|
||||
"@edx/paragon": "20.46.2",
|
||||
"@fortawesome/fontawesome-svg-core": "1.2.36",
|
||||
"@fortawesome/free-brands-svg-icons": "5.15.4",
|
||||
"@fortawesome/free-regular-svg-icons": "5.15.4",
|
||||
"@fortawesome/free-solid-svg-icons": "5.15.4",
|
||||
"@fortawesome/react-fontawesome": "0.2.0",
|
||||
"@tensorflow-models/blazeface": "0.0.7",
|
||||
"@tensorflow/tfjs-converter": "3.21.0",
|
||||
"@tensorflow/tfjs-core": "3.21.0",
|
||||
"bowser": "2.11.0",
|
||||
"classnames": "2.3.2",
|
||||
"core-js": "3.31.1",
|
||||
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
|
||||
"@edx/frontend-component-footer": "^14.6.0",
|
||||
"@edx/frontend-component-header": "^6.2.0",
|
||||
"@edx/frontend-platform": "^8.4.0",
|
||||
"@edx/openedx-atlas": "^0.7.0",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.6.0",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.6.0",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.6.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.6.0",
|
||||
"@fortawesome/react-fontawesome": "0.2.6",
|
||||
"@openedx/frontend-plugin-framework": "^1.7.0",
|
||||
"@openedx/paragon": "^23.4.5",
|
||||
"@tensorflow-models/blazeface": "0.1.0",
|
||||
"@tensorflow/tfjs-converter": "4.22.0",
|
||||
"@tensorflow/tfjs-core": "4.22.0",
|
||||
"bowser": "2.12.1",
|
||||
"classnames": "2.5.1",
|
||||
"core-js": "3.46.0",
|
||||
"font-awesome": "4.7.0",
|
||||
"form-urlencoded": "6.1.0",
|
||||
"form-urlencoded": "6.1.6",
|
||||
"formdata-polyfill": "4.0.10",
|
||||
"jslib-html5-camera-photo": "3.3.4",
|
||||
"lodash.camelcase": "4.3.0",
|
||||
@@ -57,38 +61,34 @@
|
||||
"lodash.pick": "4.4.0",
|
||||
"lodash.pickby": "4.6.0",
|
||||
"lodash.snakecase": "4.1.1",
|
||||
"long": "5.2.3",
|
||||
"memoize-one": "5.2.1",
|
||||
"long": "5.3.2",
|
||||
"memoize-one": "^6.0.0",
|
||||
"prop-types": "15.8.1",
|
||||
"qs": "6.11.2",
|
||||
"react": "17.0.2",
|
||||
"react-dom": "17.0.2",
|
||||
"qs": "6.14.0",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react-helmet": "6.1.0",
|
||||
"react-redux": "7.2.9",
|
||||
"react-router": "6.14.2",
|
||||
"react-router-dom": "6.14.2",
|
||||
"react-router": "^6.25.1",
|
||||
"react-router-dom": "^6.25.1",
|
||||
"react-router-hash-link": "2.4.3",
|
||||
"react-scrollspy": "3.4.3",
|
||||
"react-transition-group": "4.4.5",
|
||||
"redux": "4.2.1",
|
||||
"redux-devtools-extension": "2.13.9",
|
||||
"redux-logger": "3.0.6",
|
||||
"redux-saga": "1.2.3",
|
||||
"redux-saga": "1.3.0",
|
||||
"redux-thunk": "2.4.2",
|
||||
"regenerator-runtime": "0.13.11",
|
||||
"reselect": "4.1.8",
|
||||
"universal-cookie": "4.0.4"
|
||||
"regenerator-runtime": "0.14.1",
|
||||
"reselect": "^5.1.1",
|
||||
"universal-cookie": "7.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@edx/browserslist-config": "1.2.0",
|
||||
"@edx/frontend-build": "12.9.10",
|
||||
"@edx/reactifex": "1.1.0",
|
||||
"@testing-library/jest-dom": "5.17.0",
|
||||
"@testing-library/react": "12.1.5",
|
||||
"@wojtekmaj/enzyme-adapter-react-17": "0.8.0",
|
||||
"enzyme": "3.11.0",
|
||||
"react-test-renderer": "17.0.2",
|
||||
"reactifex": "1.1.1",
|
||||
"redux-mock-store": "1.5.4"
|
||||
"@edx/browserslist-config": "1.5.0",
|
||||
"@openedx/frontend-build": "^14.6.2",
|
||||
"@testing-library/jest-dom": "6.9.1",
|
||||
"@testing-library/react": "14.3.1",
|
||||
"react-test-renderer": "^18.3.1",
|
||||
"redux-mock-store": "1.5.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"automerge": true
|
||||
},
|
||||
{
|
||||
"matchPackagePatterns": ["@edx"],
|
||||
"matchPackagePatterns": ["@edx", "@openedx"],
|
||||
"matchUpdateTypes": ["minor", "patch"],
|
||||
"automerge": true
|
||||
}
|
||||
|
||||
@@ -14,9 +14,9 @@ import {
|
||||
getLanguageList,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Button, Hyperlink, Icon, Alert,
|
||||
} from '@edx/paragon';
|
||||
import { CheckCircle, Error, WarningFilled } from '@edx/paragon/icons';
|
||||
Container, Hyperlink, Icon, Alert,
|
||||
} from '@openedx/paragon';
|
||||
import { CheckCircle, Error, WarningFilled } from '@openedx/paragon/icons';
|
||||
|
||||
import messages from './AccountSettingsPage.messages';
|
||||
import {
|
||||
@@ -45,13 +45,15 @@ import {
|
||||
GENDER_OPTIONS,
|
||||
COUNTRY_WITH_STATES,
|
||||
COPPA_COMPLIANCE_YEAR,
|
||||
WORK_EXPERIENCE_OPTIONS,
|
||||
getStatesList,
|
||||
FIELD_LABELS,
|
||||
} from './data/constants';
|
||||
import { fetchSiteLanguages } from './site-language';
|
||||
import CoachingToggle from './coaching/CoachingToggle';
|
||||
import DemographicsSection from './demographics/DemographicsSection';
|
||||
import { fetchCourseList } from '../notification-preferences/data/thunks';
|
||||
import { fetchNotificationPreferences } from '../notification-preferences/data/thunks';
|
||||
import NotificationSettings from '../notification-preferences/NotificationSettings';
|
||||
import { withLocation, withNavigate } from './hoc';
|
||||
import AdditionalProfileFieldsSlot from '../plugin-slots/AdditionalProfileFieldsSlot';
|
||||
|
||||
class AccountSettingsPage extends React.Component {
|
||||
constructor(props, context) {
|
||||
@@ -65,8 +67,8 @@ class AccountSettingsPage extends React.Component {
|
||||
this.navLinkRefs = {
|
||||
'#basic-information': React.createRef(),
|
||||
'#profile-information': React.createRef(),
|
||||
'#demographics-information': React.createRef(),
|
||||
'#social-media': React.createRef(),
|
||||
'#notifications': React.createRef(),
|
||||
'#site-preferences': React.createRef(),
|
||||
'#linked-accounts': React.createRef(),
|
||||
'#delete-account': React.createRef(),
|
||||
@@ -74,7 +76,7 @@ class AccountSettingsPage extends React.Component {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.fetchCourseList();
|
||||
this.props.fetchNotificationPreferences();
|
||||
this.props.fetchSettings();
|
||||
this.props.fetchSiteLanguages(this.props.navigate);
|
||||
sendTrackingLogEvent('edx.user.settings.viewed', {
|
||||
@@ -122,7 +124,15 @@ class AccountSettingsPage extends React.Component {
|
||||
countryOptions: [{
|
||||
value: '',
|
||||
label: this.props.intl.formatMessage(messages['account.settings.field.country.options.empty']),
|
||||
}].concat(getCountryList(locale).map(({ code, name }) => ({ value: code, label: name }))),
|
||||
}].concat(
|
||||
this.removeDisabledCountries(
|
||||
getCountryList(locale).map(({ code, name }) => ({
|
||||
value: code,
|
||||
label: name,
|
||||
disabled: this.isDisabledCountry(code),
|
||||
})),
|
||||
),
|
||||
),
|
||||
stateOptions: [{
|
||||
value: '',
|
||||
label: this.props.intl.formatMessage(messages['account.settings.field.state.options.empty']),
|
||||
@@ -143,14 +153,47 @@ class AccountSettingsPage extends React.Component {
|
||||
value: key,
|
||||
label: this.props.intl.formatMessage(messages[`account.settings.field.gender.options.${key || 'empty'}`]),
|
||||
})),
|
||||
workExperienceOptions: WORK_EXPERIENCE_OPTIONS.map(key => ({
|
||||
value: key,
|
||||
label: key === '' ? this.props.intl.formatMessage(messages['account.settings.field.work.experience.options.empty']) : key,
|
||||
})),
|
||||
}));
|
||||
|
||||
canDeleteAccount = () => {
|
||||
const { committedValues } = this.props;
|
||||
return !getConfig().COUNTRIES_WITH_DELETE_ACCOUNT_DISABLED.includes(committedValues.country);
|
||||
};
|
||||
|
||||
removeDisabledCountries = (countryList) => {
|
||||
const { countriesCodesList, committedValues } = this.props;
|
||||
const committedCountry = committedValues?.country;
|
||||
|
||||
if (!countriesCodesList.length) {
|
||||
return countryList;
|
||||
}
|
||||
return countryList.filter(({ value }) => value === committedCountry || countriesCodesList.find(x => x === value));
|
||||
};
|
||||
|
||||
handleEditableFieldChange = (name, value) => {
|
||||
this.props.updateDraft(name, value);
|
||||
};
|
||||
|
||||
handleSubmit = (formId, values) => {
|
||||
this.props.saveSettings(formId, values);
|
||||
if (formId === FIELD_LABELS.COUNTRY && this.isDisabledCountry(values)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { formValues } = this.props;
|
||||
let extendedProfileObject = {};
|
||||
|
||||
if ('extended_profile' in formValues && formValues.extended_profile.some((field) => field.field_name === formId)) {
|
||||
extendedProfileObject = {
|
||||
extended_profile: formValues.extended_profile.map(field => (field.field_name === formId
|
||||
? { ...field, field_value: values }
|
||||
: field)),
|
||||
};
|
||||
}
|
||||
this.props.saveSettings(formId, values, extendedProfileObject);
|
||||
};
|
||||
|
||||
handleSubmitProfileName = (formId, values) => {
|
||||
@@ -181,6 +224,12 @@ class AccountSettingsPage extends React.Component {
|
||||
}
|
||||
};
|
||||
|
||||
isDisabledCountry = (country) => {
|
||||
const { countriesCodesList } = this.props;
|
||||
|
||||
return countriesCodesList.length > 0 && !countriesCodesList.find(x => x === country);
|
||||
};
|
||||
|
||||
isEditable(fieldName) {
|
||||
return !this.props.staticFields.includes(fieldName);
|
||||
}
|
||||
@@ -299,19 +348,9 @@ class AccountSettingsPage extends React.Component {
|
||||
header={this.props.intl.formatMessage(messages['account.settings.field.name.verified.failure.message.header'])}
|
||||
body={
|
||||
(
|
||||
<>
|
||||
<div className="d-flex flex-row">
|
||||
{this.props.intl.formatMessage(messages['account.settings.field.name.verified.failure.message'])}
|
||||
</div>
|
||||
<div className="d-flex flex-row-reverse mt-3">
|
||||
<Button
|
||||
variant="primary"
|
||||
href="https://support.edx.org/hc/en-us/articles/360004381594-Why-was-my-ID-verification-denied"
|
||||
>
|
||||
{this.props.intl.formatMessage(messages['account.settings.field.name.verified.failure.message.help.link'])}
|
||||
</Button>{' '}
|
||||
</div>
|
||||
</>
|
||||
<div className="d-flex flex-row">
|
||||
{this.props.intl.formatMessage(messages['account.settings.field.name.verified.failure.message'])}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
/>
|
||||
@@ -446,16 +485,6 @@ class AccountSettingsPage extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
renderDemographicsSection() {
|
||||
// check the result of an LMS API call to determine if we should render the DemographicsSection component
|
||||
if (this.props.formValues.shouldDisplayDemographicsSection) {
|
||||
return (
|
||||
<DemographicsSection forwardRef={this.navLinkRefs['#demographics-information']} />
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
renderContent() {
|
||||
const editableFieldProps = {
|
||||
onChange: this.handleEditableFieldChange,
|
||||
@@ -470,13 +499,16 @@ class AccountSettingsPage extends React.Component {
|
||||
yearOfBirthOptions,
|
||||
educationLevelOptions,
|
||||
genderOptions,
|
||||
workExperienceOptions,
|
||||
} = this.getLocalizedOptions(this.context.locale, this.props.formValues.country);
|
||||
|
||||
// Show State field only if the country is US (could include Canada later)
|
||||
const showState = this.props.formValues.country === COUNTRY_WITH_STATES;
|
||||
|
||||
const { country } = this.props.formValues;
|
||||
const showState = country === COUNTRY_WITH_STATES && !this.isDisabledCountry(country);
|
||||
const { verifiedName } = this.props;
|
||||
|
||||
const hasWorkExperience = !!this.props.formValues?.extended_profile?.find(field => field.field_name === 'work_experience');
|
||||
|
||||
const timeZoneOptions = this.getLocalizedTimeZoneOptions(
|
||||
this.props.timeZoneOptions,
|
||||
this.props.countryTimeZoneOptions,
|
||||
@@ -680,6 +712,18 @@ class AccountSettingsPage extends React.Component {
|
||||
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.gender.empty'])}
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
{hasWorkExperience
|
||||
&& (
|
||||
<EditableSelectField
|
||||
name="work_experience"
|
||||
type="select"
|
||||
value={this.props.formValues?.extended_profile?.find(field => field.field_name === 'work_experience')?.field_value}
|
||||
options={workExperienceOptions}
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.work.experience'])}
|
||||
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.work.experience.empty'])}
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
)}
|
||||
<EditableSelectField
|
||||
name="language_proficiencies"
|
||||
type="select"
|
||||
@@ -689,18 +733,10 @@ class AccountSettingsPage extends React.Component {
|
||||
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.language.proficiencies.empty'])}
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
{getConfig().COACHING_ENABLED
|
||||
&& this.props.formValues.coaching.eligible_for_coaching
|
||||
&& (
|
||||
<CoachingToggle
|
||||
name="coaching"
|
||||
phone_number={this.props.formValues.phone_number}
|
||||
coaching={this.props.formValues.coaching}
|
||||
/>
|
||||
)}
|
||||
|
||||
<AdditionalProfileFieldsSlot />
|
||||
</div>
|
||||
{getConfig().ENABLE_DEMOGRAPHICS_COLLECTION && this.renderDemographicsSection()}
|
||||
<div className="account-section pt-3 mb-5" id="social-media">
|
||||
<div className="account-section pt-3 mb-6" id="social-media">
|
||||
<h2 className="section-heading h4 mb-3">
|
||||
{this.props.intl.formatMessage(messages['account.settings.section.social.media'])}
|
||||
</h2>
|
||||
@@ -736,8 +772,11 @@ class AccountSettingsPage extends React.Component {
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="account-section pt-3 mb-5" id="site-preferences" ref={this.navLinkRefs['#site-preferences']}>
|
||||
<div className="border border-light-700" />
|
||||
<div className="mt-6" id="notifications" ref={this.navLinkRefs['#notifications']}>
|
||||
<NotificationSettings />
|
||||
</div>
|
||||
<div className="account-section mb-5" id="site-preferences" ref={this.navLinkRefs['#site-preferences']}>
|
||||
<h2 className="section-heading h4 mb-3">
|
||||
{this.props.intl.formatMessage(messages['account.settings.section.site.preferences'])}
|
||||
</h2>
|
||||
@@ -779,16 +818,15 @@ class AccountSettingsPage extends React.Component {
|
||||
<ThirdPartyAuth />
|
||||
</div>
|
||||
|
||||
{getConfig().ENABLE_ACCOUNT_DELETION
|
||||
&& (
|
||||
{getConfig().ENABLE_ACCOUNT_DELETION && (
|
||||
<div className="account-section pt-3 mb-5" id="delete-account" ref={this.navLinkRefs['#delete-account']}>
|
||||
<DeleteAccount
|
||||
isVerifiedAccount={this.props.isActive}
|
||||
hasLinkedTPA={hasLinkedTPA}
|
||||
canDeleteAccount={this.canDeleteAccount()}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -817,26 +855,24 @@ class AccountSettingsPage extends React.Component {
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className="page__account-settings container-fluid py-5">
|
||||
<Container className="page__account-settings py-5" size="xl">
|
||||
{this.renderDuplicateTpaProviderMessage()}
|
||||
<h1 className="mb-4">
|
||||
{this.props.intl.formatMessage(messages['account.settings.page.heading'])}
|
||||
</h1>
|
||||
<div>
|
||||
<div className="row">
|
||||
<div className="col-md-2">
|
||||
<JumpNav
|
||||
displayDemographicsLink={this.props.formValues.shouldDisplayDemographicsSection}
|
||||
/>
|
||||
<div className="col-md-3">
|
||||
<JumpNav />
|
||||
</div>
|
||||
<div className="col-md-10">
|
||||
<div className="col-md-9">
|
||||
{loading ? this.renderLoading() : null}
|
||||
{loaded ? this.renderContent() : null}
|
||||
{loadingError ? this.renderError() : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -855,11 +891,15 @@ AccountSettingsPage.propTypes = {
|
||||
name: PropTypes.string,
|
||||
email: PropTypes.string,
|
||||
secondary_email: PropTypes.string,
|
||||
secondary_email_enabled: PropTypes.bool,
|
||||
secondary_email_enabled: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
|
||||
year_of_birth: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
country: PropTypes.string,
|
||||
level_of_education: PropTypes.string,
|
||||
gender: PropTypes.string,
|
||||
extended_profile: PropTypes.arrayOf(PropTypes.shape({
|
||||
field_name: PropTypes.string,
|
||||
field_value: PropTypes.string,
|
||||
})),
|
||||
language_proficiencies: PropTypes.string,
|
||||
pending_name_change: PropTypes.string,
|
||||
phone_number: PropTypes.string,
|
||||
@@ -867,13 +907,7 @@ AccountSettingsPage.propTypes = {
|
||||
social_link_facebook: PropTypes.string,
|
||||
social_link_twitter: PropTypes.string,
|
||||
time_zone: PropTypes.string,
|
||||
coaching: PropTypes.shape({
|
||||
coaching_consent: PropTypes.bool.isRequired,
|
||||
user: PropTypes.number.isRequired,
|
||||
eligible_for_coaching: PropTypes.bool.isRequired,
|
||||
}),
|
||||
state: PropTypes.string,
|
||||
shouldDisplayDemographicsSection: PropTypes.bool,
|
||||
useVerifiedNameForCerts: PropTypes.bool.isRequired,
|
||||
verified_name: PropTypes.string,
|
||||
}).isRequired,
|
||||
@@ -881,6 +915,7 @@ AccountSettingsPage.propTypes = {
|
||||
name: PropTypes.string,
|
||||
useVerifiedNameForCerts: PropTypes.bool,
|
||||
verified_name: PropTypes.string,
|
||||
country: PropTypes.string,
|
||||
}),
|
||||
drafts: PropTypes.shape({}),
|
||||
formErrors: PropTypes.shape({
|
||||
@@ -913,13 +948,16 @@ AccountSettingsPage.propTypes = {
|
||||
saveSettings: PropTypes.func.isRequired,
|
||||
fetchSettings: PropTypes.func.isRequired,
|
||||
beginNameChange: PropTypes.func.isRequired,
|
||||
fetchCourseList: PropTypes.func.isRequired,
|
||||
fetchNotificationPreferences: PropTypes.func.isRequired,
|
||||
tpaProviders: PropTypes.arrayOf(PropTypes.shape({
|
||||
connected: PropTypes.bool,
|
||||
})),
|
||||
nameChangeModal: PropTypes.shape({
|
||||
formId: PropTypes.string,
|
||||
}),
|
||||
nameChangeModal: PropTypes.oneOfType([
|
||||
PropTypes.shape({
|
||||
formId: PropTypes.string,
|
||||
}),
|
||||
PropTypes.bool,
|
||||
]),
|
||||
verifiedName: PropTypes.shape({
|
||||
verified_name: PropTypes.string,
|
||||
status: PropTypes.string,
|
||||
@@ -939,6 +977,12 @@ AccountSettingsPage.propTypes = {
|
||||
),
|
||||
navigate: PropTypes.func.isRequired,
|
||||
location: PropTypes.string.isRequired,
|
||||
countriesCodesList: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
value: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
}),
|
||||
),
|
||||
};
|
||||
|
||||
AccountSettingsPage.defaultProps = {
|
||||
@@ -948,6 +992,7 @@ AccountSettingsPage.defaultProps = {
|
||||
committedValues: {
|
||||
useVerifiedNameForCerts: false,
|
||||
verified_name: null,
|
||||
country: '',
|
||||
},
|
||||
drafts: {},
|
||||
formErrors: {},
|
||||
@@ -960,14 +1005,15 @@ AccountSettingsPage.defaultProps = {
|
||||
tpaProviders: [],
|
||||
isActive: true,
|
||||
secondary_email_enabled: false,
|
||||
nameChangeModal: {},
|
||||
nameChangeModal: {} || false,
|
||||
verifiedName: null,
|
||||
mostRecentVerifiedName: {},
|
||||
verifiedNameHistory: [],
|
||||
countriesCodesList: [],
|
||||
};
|
||||
|
||||
export default withLocation(withNavigate(connect(accountSettingsPageSelector, {
|
||||
fetchCourseList,
|
||||
fetchNotificationPreferences,
|
||||
fetchSettings,
|
||||
saveSettings,
|
||||
saveMultipleSettings,
|
||||
|
||||
@@ -46,11 +46,6 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Profile Information',
|
||||
description: 'The profile information section heading.',
|
||||
},
|
||||
'account.settings.section.demographics.information': {
|
||||
id: 'account.settings.section.demographics.information',
|
||||
defaultMessage: 'Optional Information',
|
||||
description: 'The optional information section heading.',
|
||||
},
|
||||
'account.settings.section.site.preferences': {
|
||||
id: 'account.settings.section.site.preferences',
|
||||
defaultMessage: 'Site Preferences',
|
||||
@@ -146,11 +141,6 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Once your proctored exam passes review, this name will appear on your certificate and public-facing records. Verified Name cannot be changed at this time.',
|
||||
description: 'Help text for the account settings verified name field when a verified name has been submitted through proctoring and will appear on certificates.',
|
||||
},
|
||||
'account.settings.field.name.verified.verification.alert': {
|
||||
id: 'account.settings.field.name.verified.verification.help',
|
||||
defaultMessage: 'Enter your name as it appears on your unexpired student, work, or government-issued identification card.',
|
||||
description: 'Form label instructing the user to enter the name on their ID.',
|
||||
},
|
||||
'account.settings.field.full.name.help.text.submitted': {
|
||||
id: 'account.settings.field.full.name.help.text.submitted',
|
||||
defaultMessage: 'Verification has been submitted. This usually takes 48 hours or less. Full name cannot be changed at this time.',
|
||||
@@ -411,8 +401,8 @@ const messages = defineMessages({
|
||||
defaultMessage: 'No formal education',
|
||||
description: 'Selected by the user to describe their education.',
|
||||
},
|
||||
'account.settings.field.education.levels.o': {
|
||||
id: 'account.settings.field.education.levels.o',
|
||||
'account.settings.field.education.levels.other': {
|
||||
id: 'account.settings.field.education.levels.other',
|
||||
defaultMessage: 'Other education',
|
||||
description: 'Selected by the user if they have a type of education not described by the other choices.',
|
||||
},
|
||||
@@ -570,6 +560,21 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Notifications',
|
||||
description: 'Label for Notifications',
|
||||
},
|
||||
'account.settings.field.work.experience': {
|
||||
id: 'account.settings.work.experience',
|
||||
defaultMessage: 'Work Experience',
|
||||
description: 'Label for account settings Work experience field.',
|
||||
},
|
||||
'account.settings.field.work.experience.empty': {
|
||||
id: 'account.settings.field.work.experience.empty',
|
||||
defaultMessage: 'Add work experience',
|
||||
description: 'Placeholder for empty account settings work experience field.',
|
||||
},
|
||||
'account.settings.field.work.experience.options.empty': {
|
||||
id: 'account.settings.field.work.experience.options.empty',
|
||||
defaultMessage: 'Select work experience',
|
||||
description: 'Placeholder for the work experience levels dropdown.',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import { AppContext } from '@edx/frontend-platform/react';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { connect } from 'react-redux';
|
||||
import { Button, Hyperlink } from '@edx/paragon';
|
||||
import { Button, Hyperlink } from '@openedx/paragon';
|
||||
|
||||
import { betaLanguageBannerSelector } from './data/selectors';
|
||||
import messages from './AccountSettingsPage.messages';
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Form, StatefulButton, ModalDialog, ActionRow, useToggle, Button,
|
||||
} from '@edx/paragon';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
} from '@openedx/paragon';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { connect, useDispatch } from 'react-redux';
|
||||
import messages from './AccountSettingsPage.messages';
|
||||
import { YEAR_OF_BIRTH_OPTIONS } from './data/constants';
|
||||
@@ -11,11 +11,11 @@ import { editableFieldSelector } from './data/selectors';
|
||||
import { saveSettingsReset } from './data/actions';
|
||||
|
||||
const DOBModal = (props) => {
|
||||
const intl = useIntl();
|
||||
const {
|
||||
saveState,
|
||||
error,
|
||||
onSubmit,
|
||||
intl,
|
||||
} = props;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
@@ -56,7 +56,7 @@ const DOBModal = (props) => {
|
||||
function renderErrors() {
|
||||
if (saveState === 'error' || error) {
|
||||
return (
|
||||
<Form.Control.Feedback type="invalid" key="general-error">
|
||||
<Form.Control.Feedback type="invalid" key="general-error" data-testid="error-message">
|
||||
{intl.formatMessage(messages['account.settingsfield.dob.error.general'])}
|
||||
</Form.Control.Feedback>
|
||||
);
|
||||
@@ -72,7 +72,7 @@ const DOBModal = (props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button variant="primary" onClick={open}>
|
||||
<Button variant="primary" onClick={open} data-testid="open-modal-button">
|
||||
{intl.formatMessage(messages['account.settings.field.dob.form.button'])}
|
||||
</Button>
|
||||
<ModalDialog
|
||||
@@ -81,25 +81,27 @@ const DOBModal = (props) => {
|
||||
onClose={handleClose}
|
||||
hasCloseButton={false}
|
||||
variant="default"
|
||||
data-testid="dob-modal"
|
||||
>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<form onSubmit={handleSubmit} data-testid="dob-form">
|
||||
|
||||
<ModalDialog.Header>
|
||||
<ModalDialog.Title>
|
||||
<ModalDialog.Title data-testid="modal-title">
|
||||
{intl.formatMessage(messages['account.settings.field.dob.form.title'])}
|
||||
</ModalDialog.Title>
|
||||
</ModalDialog.Header>
|
||||
|
||||
<ModalDialog.Body className="overflow-hidden" style={{ padding: '1.5rem' }}>
|
||||
<p>{intl.formatMessage(messages['account.settings.field.dob.form.help.text'])}</p>
|
||||
<p data-testid="help-text">{intl.formatMessage(messages['account.settings.field.dob.form.help.text'])}</p>
|
||||
<Form.Group>
|
||||
<Form.Label>
|
||||
<Form.Label data-testid="month-label">
|
||||
{intl.formatMessage(messages['account.settings.field.dob.month'])}
|
||||
</Form.Label>
|
||||
<Form.Control
|
||||
as="select"
|
||||
name="month"
|
||||
onChange={handleChange}
|
||||
data-testid="month-select"
|
||||
>
|
||||
<option value="">{intl.formatMessage(messages['account.settings.field.dob.month.default'])}</option>
|
||||
{[...Array(12).keys()].map(month => (
|
||||
@@ -108,13 +110,14 @@ const DOBModal = (props) => {
|
||||
</Form.Control>
|
||||
</Form.Group>
|
||||
<Form.Group>
|
||||
<Form.Label>
|
||||
<Form.Label data-testid="year-label">
|
||||
{intl.formatMessage(messages['account.settings.field.dob.year'])}
|
||||
</Form.Label>
|
||||
<Form.Control
|
||||
as="select"
|
||||
name="year"
|
||||
onChange={handleChange}
|
||||
data-testid="year-select"
|
||||
>
|
||||
<option value="">{intl.formatMessage(messages['account.settings.field.dob.year.default'])}</option>
|
||||
{YEAR_OF_BIRTH_OPTIONS.map(year => (
|
||||
@@ -127,7 +130,7 @@ const DOBModal = (props) => {
|
||||
|
||||
<ModalDialog.Footer>
|
||||
<ActionRow>
|
||||
<ModalDialog.CloseButton variant="tertiary">
|
||||
<ModalDialog.CloseButton variant="tertiary" data-testid="cancel-button">
|
||||
Cancel
|
||||
</ModalDialog.CloseButton>
|
||||
<StatefulButton
|
||||
@@ -137,6 +140,7 @@ const DOBModal = (props) => {
|
||||
default: intl.formatMessage(messages['account.settings.editable.field.action.save']),
|
||||
}}
|
||||
disabledStates={['unedited']}
|
||||
data-testid="submit-button"
|
||||
/>
|
||||
</ActionRow>
|
||||
</ModalDialog.Footer>
|
||||
@@ -151,7 +155,6 @@ DOBModal.propTypes = {
|
||||
saveState: PropTypes.oneOf(['default', 'pending', 'complete', 'error']),
|
||||
error: PropTypes.string,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
DOBModal.defaultProps = {
|
||||
@@ -159,4 +162,4 @@ DOBModal.defaultProps = {
|
||||
error: undefined,
|
||||
};
|
||||
|
||||
export default connect(editableFieldSelector)(injectIntl(DOBModal));
|
||||
export default connect(editableFieldSelector)(DOBModal);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import classNames from 'classnames';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Button, Form, StatefulButton,
|
||||
} from '@edx/paragon';
|
||||
} from '@openedx/paragon';
|
||||
import { faPencilAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
@@ -38,10 +38,10 @@ const EditableField = (props) => {
|
||||
isEditing,
|
||||
isEditable,
|
||||
isGrayedOut,
|
||||
intl,
|
||||
...others
|
||||
} = props;
|
||||
const id = `field-${name}`;
|
||||
const intl = useIntl();
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
@@ -84,9 +84,13 @@ const EditableField = (props) => {
|
||||
if (!confirmationMessageDefinition || !confirmationValue) {
|
||||
return null;
|
||||
}
|
||||
return intl.formatMessage(confirmationMessageDefinition, {
|
||||
value: confirmationValue,
|
||||
});
|
||||
return (
|
||||
<span data-testid="editable-field-confirmation">
|
||||
{intl.formatMessage(confirmationMessageDefinition, {
|
||||
value: confirmationValue,
|
||||
})}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -95,7 +99,7 @@ const EditableField = (props) => {
|
||||
cases={{
|
||||
editing: (
|
||||
<>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<form onSubmit={handleSubmit} data-testid="editable-field-form">
|
||||
<Form.Group
|
||||
controlId={id}
|
||||
isInvalid={error != null}
|
||||
@@ -108,10 +112,11 @@ const EditableField = (props) => {
|
||||
type={type}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
data-testid="editable-field-textbox"
|
||||
{...others}
|
||||
/>
|
||||
{!!helpText && <Form.Text>{helpText}</Form.Text>}
|
||||
{error != null && <Form.Control.Feedback hasIcon={false}>{error}</Form.Control.Feedback>}
|
||||
{error != null && <Form.Control.Feedback hasIcon={false} data-testid="editable-field-error">{error}</Form.Control.Feedback>}
|
||||
{others.children}
|
||||
</Form.Group>
|
||||
<p>
|
||||
@@ -133,16 +138,21 @@ const EditableField = (props) => {
|
||||
if (saveState === 'pending') { e.preventDefault(); }
|
||||
}}
|
||||
disabledStates={[]}
|
||||
data-testid="editable-field-save"
|
||||
/>
|
||||
<Button
|
||||
variant="outline-primary"
|
||||
onClick={handleCancel}
|
||||
data-testid="editable-field-cancel"
|
||||
data-clicked="cancel"
|
||||
>
|
||||
{intl.formatMessage(messages['account.settings.editable.field.action.cancel'])}
|
||||
</Button>
|
||||
</p>
|
||||
</form>
|
||||
{['name', 'verified_name'].includes(name) && <CertificatePreference fieldName={name} />}
|
||||
{['name', 'verified_name'].includes(name) && (
|
||||
<CertificatePreference fieldName={name} data-testid="editable-field-certificate-preference" />
|
||||
)}
|
||||
</>
|
||||
),
|
||||
default: (
|
||||
@@ -150,12 +160,12 @@ const EditableField = (props) => {
|
||||
<div className="d-flex align-items-start">
|
||||
<h6 aria-level="3">{label}</h6>
|
||||
{isEditable ? (
|
||||
<Button variant="link" onClick={handleEdit} className="ml-3">
|
||||
<Button variant="link" onClick={handleEdit} className="ml-3" data-testid="editable-field-edit" data-clicked="edit">
|
||||
<FontAwesomeIcon className="mr-1" icon={faPencilAlt} />{intl.formatMessage(messages['account.settings.editable.field.action.edit'])}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
<p data-hj-suppress className={isGrayedOut ? 'grayed-out' : null}>{renderValue(value)}</p>
|
||||
<p data-hj-suppress className={classNames('text-truncate', { 'grayed-out': isGrayedOut })}>{renderValue(value)}</p>
|
||||
<p className="small text-muted mt-n2">{renderConfirmationMessage() || helpText}</p>
|
||||
</div>
|
||||
),
|
||||
@@ -187,7 +197,6 @@ EditableField.propTypes = {
|
||||
isEditing: PropTypes.bool,
|
||||
isEditable: PropTypes.bool,
|
||||
isGrayedOut: PropTypes.bool,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
EditableField.defaultProps = {
|
||||
@@ -208,4 +217,4 @@ EditableField.defaultProps = {
|
||||
export default connect(editableFieldSelector, {
|
||||
onEdit: openForm,
|
||||
onCancel: closeForm,
|
||||
})(injectIntl(EditableField));
|
||||
})(EditableField);
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Button, Form, StatefulButton,
|
||||
} from '@edx/paragon';
|
||||
} from '@openedx/paragon';
|
||||
import { faPencilAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
@@ -19,6 +18,7 @@ import { editableFieldSelector } from './data/selectors';
|
||||
import CertificatePreference from './certificate-preference/CertificatePreference';
|
||||
|
||||
const EditableSelectField = (props) => {
|
||||
const intl = useIntl();
|
||||
const {
|
||||
name,
|
||||
label,
|
||||
@@ -39,7 +39,6 @@ const EditableSelectField = (props) => {
|
||||
isEditing,
|
||||
isEditable,
|
||||
isGrayedOut,
|
||||
intl,
|
||||
...others
|
||||
} = props;
|
||||
const id = `field-${name}`;
|
||||
@@ -98,9 +97,29 @@ const EditableSelectField = (props) => {
|
||||
value: confirmationValue,
|
||||
});
|
||||
};
|
||||
const selectOptions = options.map(option => (
|
||||
<option value={option.value} key={`${option.value}-${option.label}`}>{option.label}</option>
|
||||
));
|
||||
const selectOptions = options.map((option) => {
|
||||
if (option.group) {
|
||||
// If the option has a 'group' property, it represents an element with sub-options.
|
||||
return (
|
||||
<optgroup label={option.label} key={option.label}>
|
||||
{option.group.map((subOption) => (
|
||||
<option
|
||||
value={subOption.value}
|
||||
key={`${subOption.value}-${subOption.label}`}
|
||||
disabled={subOption?.disabled}
|
||||
>
|
||||
{subOption.label}
|
||||
</option>
|
||||
))}
|
||||
</optgroup>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<option value={option.value} key={`${option.value}-${option.label}`} disabled={option?.disabled}>
|
||||
{option.label}
|
||||
</option>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<SwitchContent
|
||||
@@ -207,7 +226,6 @@ EditableSelectField.propTypes = {
|
||||
isEditing: PropTypes.bool,
|
||||
isEditable: PropTypes.bool,
|
||||
isGrayedOut: PropTypes.bool,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
EditableSelectField.defaultProps = {
|
||||
@@ -229,4 +247,4 @@ EditableSelectField.defaultProps = {
|
||||
export default connect(editableFieldSelector, {
|
||||
onEdit: openForm,
|
||||
onCancel: closeForm,
|
||||
})(injectIntl(EditableSelectField));
|
||||
})(EditableSelectField);
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl, FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Button, StatefulButton, Form,
|
||||
} from '@edx/paragon';
|
||||
Button, StatefulButton, Form, Tooltip, OverlayTrigger,
|
||||
} from '@openedx/paragon';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faExclamationTriangle, faPencilAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
@@ -35,9 +34,9 @@ const EmailField = (props) => {
|
||||
onChange,
|
||||
isEditing,
|
||||
isEditable,
|
||||
intl,
|
||||
} = props;
|
||||
const id = `field-${name}`;
|
||||
const intl = useIntl();
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
@@ -162,7 +161,16 @@ const EmailField = (props) => {
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
<p data-hj-suppress>{renderValue()}</p>
|
||||
<OverlayTrigger
|
||||
placement="top"
|
||||
overlay={(
|
||||
<Tooltip id={`tooltip-${name}`} variant="light" className="d-sm-none">
|
||||
{renderValue()}
|
||||
</Tooltip>
|
||||
)}
|
||||
>
|
||||
<p data-hj-suppress className="text-truncate">{renderValue()}</p>
|
||||
</OverlayTrigger>
|
||||
{renderConfirmationMessage() || <p className="small text-muted mt-n2">{helpText}</p>}
|
||||
</div>
|
||||
),
|
||||
@@ -191,7 +199,6 @@ EmailField.propTypes = {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
isEditing: PropTypes.bool,
|
||||
isEditable: PropTypes.bool,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
EmailField.defaultProps = {
|
||||
@@ -210,4 +217,4 @@ EmailField.defaultProps = {
|
||||
export default connect(editableFieldSelector, {
|
||||
onEdit: openForm,
|
||||
onCancel: closeForm,
|
||||
})(injectIntl(EmailField));
|
||||
})(EmailField);
|
||||
|
||||
@@ -1,38 +1,30 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { breakpoints, useWindowSize, Icon } from '@edx/paragon';
|
||||
import { OpenInNew } from '@edx/paragon/icons';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { breakpoints, useWindowSize } from '@openedx/paragon';
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { NavHashLink } from 'react-router-hash-link';
|
||||
import Scrollspy from 'react-scrollspy';
|
||||
import { Link } from 'react-router-dom';
|
||||
import messages from './AccountSettingsPage.messages';
|
||||
import { selectShowPreferences } from '../notification-preferences/data/selectors';
|
||||
|
||||
const JumpNav = ({
|
||||
intl,
|
||||
displayDemographicsLink,
|
||||
}) => {
|
||||
const JumpNav = () => {
|
||||
const intl = useIntl();
|
||||
const stickToTop = useWindowSize().width > breakpoints.small.minWidth;
|
||||
const showPreferences = useSelector(selectShowPreferences());
|
||||
|
||||
return (
|
||||
<div className={classNames('jump-nav px-2.25', { 'jump-nav-sm position-sticky pt-3': stickToTop })}>
|
||||
<div className={classNames('jump-nav', { 'jump-nav-sm position-sticky pt-3': stickToTop })}>
|
||||
<Scrollspy
|
||||
items={[
|
||||
'basic-information',
|
||||
'profile-information',
|
||||
'demographics-information',
|
||||
'social-media',
|
||||
'notifications',
|
||||
'site-preferences',
|
||||
'linked-accounts',
|
||||
'delete-account',
|
||||
]}
|
||||
className="list-unstyled"
|
||||
currentClassName="font-weight-bold"
|
||||
offset={-64}
|
||||
>
|
||||
<li>
|
||||
<NavHashLink to="#basic-information">
|
||||
@@ -44,19 +36,16 @@ const JumpNav = ({
|
||||
{intl.formatMessage(messages['account.settings.section.profile.information'])}
|
||||
</NavHashLink>
|
||||
</li>
|
||||
{getConfig().ENABLE_DEMOGRAPHICS_COLLECTION && displayDemographicsLink
|
||||
&& (
|
||||
<li>
|
||||
<NavHashLink to="#demographics-information">
|
||||
{intl.formatMessage(messages['account.settings.section.demographics.information'])}
|
||||
</NavHashLink>
|
||||
</li>
|
||||
)}
|
||||
<li>
|
||||
<NavHashLink to="#social-media">
|
||||
{intl.formatMessage(messages['account.settings.section.social.media'])}
|
||||
</NavHashLink>
|
||||
</li>
|
||||
<li>
|
||||
<NavHashLink to="#notifications">
|
||||
{intl.formatMessage(messages['notification.preferences.notifications.label'])}
|
||||
</NavHashLink>
|
||||
</li>
|
||||
<li>
|
||||
<NavHashLink to="#site-preferences">
|
||||
{intl.formatMessage(messages['account.settings.section.site.preferences'])}
|
||||
@@ -76,28 +65,8 @@ const JumpNav = ({
|
||||
</li>
|
||||
)}
|
||||
</Scrollspy>
|
||||
{showPreferences && (
|
||||
<>
|
||||
<hr />
|
||||
<Scrollspy
|
||||
className="list-unstyled"
|
||||
>
|
||||
<li>
|
||||
<Link to="/notifications" target="_blank" rel="noopener noreferrer">
|
||||
<span>{intl.formatMessage(messages['notification.preferences.notifications.label'])}</span>
|
||||
<Icon className="d-inline-block align-bottom ml-1" src={OpenInNew} />
|
||||
</Link>
|
||||
</li>
|
||||
</Scrollspy>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
JumpNav.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
displayDemographicsLink: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(JumpNav);
|
||||
export default JumpNav;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Alert } from '@edx/paragon';
|
||||
import { Alert } from '@openedx/paragon';
|
||||
|
||||
const OneTimeDismissibleAlert = (props) => {
|
||||
const [dismissed, setDismissed] = useState(localStorage.getItem(props.id) !== 'true');
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { TransitionReplace } from '@edx/paragon';
|
||||
import { TransitionReplace } from '@openedx/paragon';
|
||||
|
||||
const onChildExit = (htmlNode) => {
|
||||
// If the leaving child has focus, take control and redirect it
|
||||
|
||||
@@ -29,7 +29,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.custom-switch {
|
||||
padding: 0;
|
||||
max-width: 500px;
|
||||
@@ -44,3 +43,8 @@
|
||||
filter: alpha(opacity = 60); /* MSIE */
|
||||
}
|
||||
}
|
||||
|
||||
#tooltip-email .small {
|
||||
display: block;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { connect, useDispatch } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
@@ -7,8 +7,8 @@ import {
|
||||
Form,
|
||||
ModalDialog,
|
||||
StatefulButton,
|
||||
} from '@edx/paragon';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
} from '@openedx/paragon';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import {
|
||||
closeForm,
|
||||
@@ -22,7 +22,6 @@ import commonMessages from '../AccountSettingsPage.messages';
|
||||
import messages from './messages';
|
||||
|
||||
const CertificatePreference = ({
|
||||
intl,
|
||||
fieldName,
|
||||
originalFullName,
|
||||
originalVerifiedName,
|
||||
@@ -33,6 +32,7 @@ const CertificatePreference = ({
|
||||
const [checked, setChecked] = useState(false);
|
||||
const [modalIsOpen, setModalIsOpen] = useState(false);
|
||||
const formId = 'useVerifiedNameForCerts';
|
||||
const intl = useIntl();
|
||||
|
||||
const handleCheckboxChange = () => {
|
||||
if (!checked) {
|
||||
@@ -155,7 +155,6 @@ const CertificatePreference = ({
|
||||
};
|
||||
|
||||
CertificatePreference.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
fieldName: PropTypes.string.isRequired,
|
||||
originalFullName: PropTypes.string,
|
||||
originalVerifiedName: PropTypes.string,
|
||||
@@ -170,4 +169,4 @@ CertificatePreference.defaultProps = {
|
||||
useVerifiedNameForCerts: false,
|
||||
};
|
||||
|
||||
export default connect(certPreferenceSelector)(injectIntl(CertificatePreference));
|
||||
export default connect(certPreferenceSelector)(CertificatePreference);
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
|
||||
import { postVerifiedNameConfig } from './service';
|
||||
import { handleRequestError } from '../../data/utils';
|
||||
|
||||
jest.mock('@edx/frontend-platform');
|
||||
jest.mock('@edx/frontend-platform/auth');
|
||||
jest.mock('../../data/utils');
|
||||
|
||||
describe('postVerifiedNameConfig', () => {
|
||||
const mockPost = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
|
||||
getConfig.mockReturnValue({
|
||||
LMS_BASE_URL: 'http://testserver',
|
||||
});
|
||||
|
||||
getAuthenticatedHttpClient.mockReturnValue({
|
||||
post: mockPost,
|
||||
});
|
||||
});
|
||||
|
||||
it('posts verified name config with useVerifiedNameForCerts = true', async () => {
|
||||
const mockResponse = { data: { success: true } };
|
||||
mockPost.mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const result = await postVerifiedNameConfig('testuser', { useVerifiedNameForCerts: true });
|
||||
|
||||
expect(getConfig).toHaveBeenCalled();
|
||||
expect(getAuthenticatedHttpClient).toHaveBeenCalled();
|
||||
expect(mockPost).toHaveBeenCalledWith(
|
||||
'http://testserver/api/edx_name_affirmation/v1/verified_name/config',
|
||||
{
|
||||
username: 'testuser',
|
||||
use_verified_name_for_certs: true,
|
||||
},
|
||||
{ headers: { Accept: 'application/json' } },
|
||||
);
|
||||
expect(result).toEqual(mockResponse.data);
|
||||
});
|
||||
|
||||
it('posts verified name config with useVerifiedNameForCerts = false', async () => {
|
||||
const mockResponse = { data: { success: false } };
|
||||
mockPost.mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const result = await postVerifiedNameConfig('anotheruser', { useVerifiedNameForCerts: false });
|
||||
|
||||
expect(mockPost).toHaveBeenCalledWith(
|
||||
'http://testserver/api/edx_name_affirmation/v1/verified_name/config',
|
||||
{
|
||||
username: 'anotheruser',
|
||||
use_verified_name_for_certs: false,
|
||||
},
|
||||
{ headers: { Accept: 'application/json' } },
|
||||
);
|
||||
expect(result).toEqual(mockResponse.data);
|
||||
});
|
||||
|
||||
it('calls handleRequestError and throws when request fails', async () => {
|
||||
const mockError = new Error('Request failed');
|
||||
mockPost.mockRejectedValueOnce(mockError);
|
||||
|
||||
handleRequestError.mockImplementation(() => {
|
||||
throw mockError;
|
||||
});
|
||||
|
||||
await expect(
|
||||
postVerifiedNameConfig('erroruser', { useVerifiedNameForCerts: true }),
|
||||
).rejects.toThrow('Request failed');
|
||||
|
||||
expect(handleRequestError).toHaveBeenCalledWith(mockError);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,5 @@
|
||||
/* eslint-disable no-import-assign */
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import configureStore from 'redux-mock-store';
|
||||
@@ -11,10 +10,14 @@ import {
|
||||
} from '@testing-library/react';
|
||||
|
||||
import * as auth from '@edx/frontend-platform/auth';
|
||||
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import messages from '../messages';
|
||||
|
||||
// Modal creates a portal. Overriding ReactDOM.createPortal allows portals to be tested in jest.
|
||||
ReactDOM.createPortal = node => node;
|
||||
// Modal creates a portal. Overriding createPortal allows portals to be tested in jest.
|
||||
jest.mock('react-dom', () => ({
|
||||
...jest.requireActual('react-dom'),
|
||||
createPortal: jest.fn(node => node), // Mock portal behavior
|
||||
}));
|
||||
|
||||
import CertificatePreference from '../CertificatePreference'; // eslint-disable-line import/first
|
||||
|
||||
@@ -27,8 +30,6 @@ jest.mock('react-redux', () => ({
|
||||
jest.mock('@edx/frontend-platform/auth');
|
||||
jest.mock('../../data/selectors', () => jest.fn().mockImplementation(() => ({ certPreferenceSelector: () => ({}) })));
|
||||
|
||||
const IntlCertificatePreference = injectIntl(CertificatePreference);
|
||||
|
||||
const mockStore = configureStore();
|
||||
|
||||
describe('NameChange', () => {
|
||||
@@ -36,7 +37,7 @@ describe('NameChange', () => {
|
||||
let store = {};
|
||||
const formId = 'useVerifiedNameForCerts';
|
||||
const updateDraft = 'UPDATE_DRAFT';
|
||||
const labelText = 'If checked, this name will appear on your certificates and public-facing records.';
|
||||
const labelText = messages['account.settings.field.name.checkbox.certificate.select'].defaultMessage;
|
||||
|
||||
const reduxWrapper = children => (
|
||||
<Router>
|
||||
@@ -54,7 +55,6 @@ describe('NameChange', () => {
|
||||
originalVerifiedName: 'edX Verified',
|
||||
saveState: null,
|
||||
useVerifiedNameForCerts: false,
|
||||
intl: {},
|
||||
};
|
||||
|
||||
auth.getAuthenticatedHttpClient = jest.fn(() => ({
|
||||
@@ -74,7 +74,7 @@ describe('NameChange', () => {
|
||||
originalVerifiedName: '',
|
||||
};
|
||||
|
||||
const wrapper = render(reduxWrapper(<IntlCertificatePreference {...props} />));
|
||||
const wrapper = render(reduxWrapper(<CertificatePreference {...props} />));
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
@@ -85,7 +85,7 @@ describe('NameChange', () => {
|
||||
useVerifiedNameForCerts: true,
|
||||
};
|
||||
|
||||
render(reduxWrapper(<IntlCertificatePreference {...props} />));
|
||||
render(reduxWrapper(<CertificatePreference {...props} />));
|
||||
|
||||
const checkbox = screen.getByLabelText(labelText);
|
||||
expect(checkbox.checked).toEqual(false);
|
||||
@@ -100,7 +100,7 @@ describe('NameChange', () => {
|
||||
});
|
||||
|
||||
it('triggers modal when attempting to uncheck checkbox', () => {
|
||||
render(reduxWrapper(<IntlCertificatePreference {...props} />));
|
||||
render(reduxWrapper(<CertificatePreference {...props} />));
|
||||
|
||||
const checkbox = screen.getByLabelText(labelText);
|
||||
expect(checkbox.checked).toEqual(true);
|
||||
@@ -112,7 +112,7 @@ describe('NameChange', () => {
|
||||
});
|
||||
|
||||
it('updates draft when changing radio value', () => {
|
||||
render(reduxWrapper(<IntlCertificatePreference {...props} />));
|
||||
render(reduxWrapper(<CertificatePreference {...props} />));
|
||||
|
||||
const checkbox = screen.getByLabelText(labelText);
|
||||
fireEvent.click(checkbox);
|
||||
@@ -130,7 +130,7 @@ describe('NameChange', () => {
|
||||
});
|
||||
|
||||
it('clears draft on cancel', () => {
|
||||
render(reduxWrapper(<IntlCertificatePreference {...props} />));
|
||||
render(reduxWrapper(<CertificatePreference {...props} />));
|
||||
|
||||
const checkbox = screen.getByLabelText(labelText);
|
||||
fireEvent.click(checkbox);
|
||||
@@ -143,7 +143,7 @@ describe('NameChange', () => {
|
||||
});
|
||||
|
||||
it('submits', () => {
|
||||
render(reduxWrapper(<IntlCertificatePreference {...props} />));
|
||||
render(reduxWrapper(<CertificatePreference {...props} />));
|
||||
|
||||
const checkbox = screen.getByLabelText(labelText);
|
||||
fireEvent.click(checkbox);
|
||||
@@ -151,7 +151,7 @@ describe('NameChange', () => {
|
||||
const submitButton = screen.getByText('Choose name');
|
||||
fireEvent.click(submitButton);
|
||||
expect(mockDispatch).toHaveBeenCalledWith({
|
||||
payload: { formId, commitValues: false },
|
||||
payload: { formId, commitValues: false, extendedProfile: {} },
|
||||
type: 'ACCOUNT_SETTINGS__SAVE_SETTINGS',
|
||||
});
|
||||
});
|
||||
@@ -163,7 +163,7 @@ describe('NameChange', () => {
|
||||
useVerifiedNameForCerts: true,
|
||||
};
|
||||
|
||||
render(reduxWrapper(<IntlCertificatePreference {...props} />));
|
||||
render(reduxWrapper(<CertificatePreference {...props} />));
|
||||
|
||||
const checkbox = screen.getByLabelText(labelText);
|
||||
expect(checkbox.checked).toEqual(true);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`NameChange does not render if there is no verified name 1`] = `
|
||||
Object {
|
||||
{
|
||||
"asFragment": [Function],
|
||||
"baseElement": <body>
|
||||
<div />
|
||||
|
||||
@@ -1,267 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig, getQueryParameters } from '@edx/frontend-platform';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink } from '@edx/paragon';
|
||||
import { faCheck } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import get from 'lodash.get';
|
||||
import { getAuthenticatedHttpClient, getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
|
||||
import PageLoading from '../PageLoading';
|
||||
import CoachingConsentForm from './CoachingConsentForm';
|
||||
import messages from './CoachingConsent.messages';
|
||||
import LogoSVG from '../../logo.svg';
|
||||
import { fetchSettings } from '../data/actions';
|
||||
import { coachingConsentPageSelector } from '../data/selectors';
|
||||
|
||||
const Logo = ({ src, alt, ...attributes }) => <img src={src} alt={alt} {...attributes} />;
|
||||
|
||||
const SuccessMessage = (props) => (
|
||||
<div className="col-12 col-lg-6 shadow-lg mx-auto mt-4 p-5">
|
||||
<FontAwesomeIcon className="text-success" icon={faCheck} size="5x" />
|
||||
<div className="h3">{props.header}</div>
|
||||
<div>{props.message}</div>
|
||||
<Hyperlink destination={props.continueUrl} className="d-block p-2 my-3 text-center text-white bg-primary rounded">
|
||||
{props.continue}
|
||||
</Hyperlink>
|
||||
</div>
|
||||
);
|
||||
|
||||
const AutoRedirect = (props) => {
|
||||
window.location.href = props.redirectUrl;
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
return <></>;
|
||||
};
|
||||
|
||||
const VIEWS = {
|
||||
NOT_LOADED: 'NOT_LOADED',
|
||||
LOADED: 'LOADED',
|
||||
SUCCESS: 'SUCCESS',
|
||||
SUCCESS_PENDING: 'SUCCESS_PENDING',
|
||||
DECLINED: 'DECLINED',
|
||||
DECLINE_PENDING: 'DECLINE_PENDING',
|
||||
};
|
||||
|
||||
class CoachingConsent extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
// Used to redirect back to the courseware.
|
||||
const nextUrl = this.sanitizeForwardingUrl(getQueryParameters().next);
|
||||
this.state = {
|
||||
redirectUrl: nextUrl || `${getConfig().LMS_BASE_URL}/dashboard/`,
|
||||
formErrors: {},
|
||||
formSubmitted: false,
|
||||
declineSubmitted: false,
|
||||
submissionSuccess: false,
|
||||
};
|
||||
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
this.declineCoaching = this.declineCoaching.bind(this);
|
||||
this.patchUsingCoachingConsentForm = this.patchUsingCoachingConsentForm.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.fetchSettings();
|
||||
}
|
||||
|
||||
handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
const fullName = e.target.fullName.value;
|
||||
const phoneNumber = e.target.phoneNumber.value;
|
||||
const body = {
|
||||
coaching_consent: true,
|
||||
consent_form_seen: true,
|
||||
phone_number: phoneNumber,
|
||||
full_name: fullName,
|
||||
};
|
||||
this.setState({
|
||||
formErrors: {},
|
||||
formSubmitted: true,
|
||||
declineSubmitted: false,
|
||||
}, () => this.patchUsingCoachingConsentForm(body));
|
||||
}
|
||||
|
||||
sanitizeForwardingUrl(url) {
|
||||
// Redirect to root of MFE if invalid next param is sent
|
||||
return url && url.startsWith(getConfig().LMS_BASE_URL) ? url : `${getConfig().LMS_BASE_URL}/dashboard/`;
|
||||
}
|
||||
|
||||
async patchUsingCoachingConsentForm(body) {
|
||||
const { userId } = getAuthenticatedUser();
|
||||
const requestUrl = `${getConfig().LMS_BASE_URL}/api/coaching/v1/coaching_consent/${userId}/`;
|
||||
let formErrors = {};
|
||||
const data = await getAuthenticatedHttpClient()
|
||||
.patch(requestUrl, body)
|
||||
.catch((error) => {
|
||||
if (get(error, 'customAttributes.httpErrorResponseData')) {
|
||||
formErrors = JSON.parse(error.customAttributes.httpErrorResponseData);
|
||||
} else {
|
||||
formErrors = { full_name: 'Something went wrong. Please try again.' };
|
||||
}
|
||||
this.setState({
|
||||
submissionSuccess: false,
|
||||
formErrors,
|
||||
formSubmitted: false,
|
||||
});
|
||||
});
|
||||
if (get(data, 'status') === 200) {
|
||||
this.setState({ submissionSuccess: true });
|
||||
}
|
||||
}
|
||||
|
||||
declineCoaching(e) {
|
||||
e.preventDefault();
|
||||
const body = {
|
||||
coaching_consent: false,
|
||||
consent_form_seen: true,
|
||||
};
|
||||
this.setState({
|
||||
formErrors: {},
|
||||
formSubmitted: false,
|
||||
declineSubmitted: true,
|
||||
}, () => this.patchUsingCoachingConsentForm(body));
|
||||
}
|
||||
|
||||
renderView(currentView) {
|
||||
switch (currentView) {
|
||||
case VIEWS.NOT_LOADED:
|
||||
return <PageLoading srMessage="" />;
|
||||
case VIEWS.LOADED:
|
||||
return (
|
||||
<CoachingConsentForm
|
||||
onSubmit={this.handleSubmit}
|
||||
declineCoaching={this.declineCoaching}
|
||||
formErrors={this.state.formErrors}
|
||||
formValues={this.props.formValues}
|
||||
redirectUrl={this.state.redirectUrl}
|
||||
profileDataManager={this.props.profileDataManager}
|
||||
/>
|
||||
);
|
||||
case VIEWS.SUCCESS_PENDING:
|
||||
return <PageLoading srMessage="Submitting..." />;
|
||||
case VIEWS.SUCCESS:
|
||||
return (
|
||||
<SuccessMessage
|
||||
continueUrl={this.state.redirectUrl}
|
||||
header={this.props.intl.formatMessage(messages['account.settings.coaching.consent.success.header'])}
|
||||
message={this.props.intl.formatMessage(messages['account.settings.coaching.consent.success.message'])}
|
||||
continue={this.props.intl.formatMessage(messages['account.settings.coaching.consent.success.continue'])}
|
||||
/>
|
||||
);
|
||||
case VIEWS.DECLINE_PENDING:
|
||||
return <PageLoading srMessage="Redirecting..." />;
|
||||
case VIEWS.DECLINED:
|
||||
return <AutoRedirect redirectUrl={this.state.redirectUrl} />;
|
||||
default:
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
return <></>;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { loaded } = this.props;
|
||||
const formHasErrors = Object.keys(this.state.formErrors).length > 0;
|
||||
let currentView = null;
|
||||
// This amount of logic was making the template very hard to read, so I broke it out into views.
|
||||
if (!loaded) {
|
||||
currentView = VIEWS.NOT_LOADED;
|
||||
} else if (this.state.formSubmitted && !formHasErrors) {
|
||||
if (this.state.submissionSuccess) {
|
||||
currentView = VIEWS.SUCCESS;
|
||||
} else {
|
||||
currentView = VIEWS.SUCCESS_PENDING;
|
||||
}
|
||||
} else if (this.state.declineSubmitted && !formHasErrors) {
|
||||
if (this.state.submissionSuccess) {
|
||||
currentView = VIEWS.DECLINED;
|
||||
} else {
|
||||
currentView = VIEWS.DECLINE_PENDING;
|
||||
}
|
||||
} else {
|
||||
currentView = VIEWS.LOADED;
|
||||
}
|
||||
|
||||
return (
|
||||
<main>
|
||||
<div className="w-100 d-flex justify-content-center align-items-center shadow coaching-header">
|
||||
<Logo
|
||||
className="logo"
|
||||
src={LogoSVG}
|
||||
alt="Logo"
|
||||
/>
|
||||
</div>
|
||||
{this.renderView(currentView)}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Logo.defaultProps = {
|
||||
src: '',
|
||||
alt: '',
|
||||
};
|
||||
|
||||
Logo.propTypes = {
|
||||
src: PropTypes.string,
|
||||
alt: PropTypes.string,
|
||||
};
|
||||
|
||||
SuccessMessage.defaultProps = {
|
||||
header: '',
|
||||
message: '',
|
||||
continueUrl: '',
|
||||
continue: '',
|
||||
};
|
||||
|
||||
SuccessMessage.propTypes = {
|
||||
header: PropTypes.string,
|
||||
message: PropTypes.string,
|
||||
continueUrl: PropTypes.string,
|
||||
continue: PropTypes.string,
|
||||
};
|
||||
|
||||
AutoRedirect.defaultProps = {
|
||||
redirectUrl: '',
|
||||
};
|
||||
|
||||
AutoRedirect.propTypes = {
|
||||
redirectUrl: PropTypes.string,
|
||||
};
|
||||
|
||||
CoachingConsent.defaultProps = {
|
||||
loaded: false,
|
||||
profileDataManager: null,
|
||||
};
|
||||
|
||||
CoachingConsent.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
loaded: PropTypes.bool,
|
||||
formValues: PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
phone_number: PropTypes.string,
|
||||
coaching: PropTypes.shape({
|
||||
coaching_consent: PropTypes.bool.isRequired,
|
||||
user: PropTypes.number.isRequired,
|
||||
eligible_for_coaching: PropTypes.bool.isRequired,
|
||||
consent_form_seen: PropTypes.bool.isRequired,
|
||||
}),
|
||||
}).isRequired,
|
||||
formErrors: PropTypes.shape({
|
||||
coaching: PropTypes.shape({}),
|
||||
}).isRequired,
|
||||
confirmationValues: PropTypes.shape({
|
||||
coaching: PropTypes.shape({}),
|
||||
name: PropTypes.shape({}),
|
||||
phone_number: PropTypes.shape({}),
|
||||
}).isRequired,
|
||||
fetchSettings: PropTypes.func.isRequired,
|
||||
profileDataManager: PropTypes.string,
|
||||
};
|
||||
|
||||
export default connect(coachingConsentPageSelector, {
|
||||
fetchSettings,
|
||||
})(injectIntl(CoachingConsent));
|
||||
@@ -1,66 +0,0 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
'account.settings.coaching.consent.welcome.header': {
|
||||
id: 'account.settings.coaching.consent.welcome.header',
|
||||
defaultMessage: 'Let’s get started.',
|
||||
description: 'The welcome header for consent form.',
|
||||
},
|
||||
'account.settings.coaching.consent.welcome.subheader': {
|
||||
id: 'account.settings.coaching.consent.welcome.subheader',
|
||||
defaultMessage: "We're here for you from start to finish",
|
||||
description: 'The welcome subheader for consent form.',
|
||||
},
|
||||
'account.settings.coaching.consent.description': {
|
||||
id: 'account.settings.coaching.consent.description',
|
||||
defaultMessage: "MicroBachelors programs include coaching that focuses on your career, education, and how you'll achieve results through one-on-one communication with an experienced professional. If you’re interested, provide the information below and click “Submit,” and our coaching partner will connect with you via email and/or text message to help you move forward. Terms and conditions apply.*",
|
||||
description: 'Text describing what Coaching is.',
|
||||
},
|
||||
'account.settings.coaching.consent.text-messaging.disclaimer': {
|
||||
id: 'account.settings.coaching.consent.text-messaging.disclaimer',
|
||||
defaultMessage: '* Coaching services are included at no additional cost to learners with US phone numbers. Coaching includes recurring text messages. Message and data rates may apply. Text STOP to opt-out.',
|
||||
description: 'Text describing what Coaching is.',
|
||||
},
|
||||
'account.settings.coaching.consent.accept-coaching': {
|
||||
id: 'account.settings.coaching.consent.accept-coaching',
|
||||
defaultMessage: 'Sign up for coaching',
|
||||
description: 'Text to confirm coaching enablement',
|
||||
},
|
||||
'account.settings.coaching.consent.decline-coaching': {
|
||||
id: 'account.settings.coaching.consent.decline-coaching',
|
||||
defaultMessage: 'I prefer not to be contacted with free coaching services',
|
||||
description: 'Text to decline coaching enablement',
|
||||
},
|
||||
'account.settings.coaching.consent.label.name': {
|
||||
id: 'account.settings.coaching.consent.label.name',
|
||||
defaultMessage: 'Please confirm your name',
|
||||
description: 'Label for name input',
|
||||
},
|
||||
'account.settings.coaching.consent.label.phone-number': {
|
||||
id: 'account.settings.coaching.consent.label.phone-number',
|
||||
defaultMessage: 'Enter your mobile number',
|
||||
description: 'Label for mobile phone number input',
|
||||
},
|
||||
'account.settings.coaching.consent.success.header': {
|
||||
id: 'account.settings.coaching.consent.success.header',
|
||||
defaultMessage: 'Success!',
|
||||
description: 'Heading announcing that submission succeeded',
|
||||
},
|
||||
'account.settings.coaching.consent.success.message': {
|
||||
id: 'account.settings.coaching.consent.success.message',
|
||||
defaultMessage: "You're signed up for coaching. You can expect a message via email or SMS in the coming days.",
|
||||
description: 'Text announcing that you have signed up and will receive texts',
|
||||
},
|
||||
'account.settings.coaching.consent.success.continue': {
|
||||
id: 'account.settings.coaching.consent.success.continue',
|
||||
defaultMessage: 'Start my course',
|
||||
description: 'Text that the user will be sent back to the courseware',
|
||||
},
|
||||
'account.settings.coaching.managed.support': {
|
||||
id: 'account.settings.coaching.managed.support',
|
||||
defaultMessage: 'support',
|
||||
description: 'website support',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -1,129 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { Form, Button, Hyperlink } from '@edx/paragon';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import Alert from '../Alert';
|
||||
import messages from './CoachingConsent.messages';
|
||||
|
||||
const ErrorMessage = (props) => <div className="alert-warning mb-2">{props.message}</div>;
|
||||
|
||||
const ManagedProfileAlert = ({ profileDataManager }) => (
|
||||
<Alert className="alert alert-primary" role="alert">
|
||||
<FormattedMessage
|
||||
id="account.settings.coaching.managed.alert"
|
||||
defaultMessage="Your name is managed by {managerTitle}. Contact your administrator for help."
|
||||
description="Alert message informing the user their account data is managed by a third party"
|
||||
values={{
|
||||
managerTitle: <b>{profileDataManager}</b>,
|
||||
}}
|
||||
/>
|
||||
</Alert>
|
||||
);
|
||||
const CoachingForm = (props) => (
|
||||
<div className="col-12 col-md-6 col-xl-5 mx-auto mt-4 p-5 shadow-lg">
|
||||
<h2 className="h2">
|
||||
{props.intl.formatMessage(messages['account.settings.coaching.consent.welcome.header'])}
|
||||
</h2>
|
||||
<p>{props.intl.formatMessage(messages['account.settings.coaching.consent.description'])}</p>
|
||||
<div>
|
||||
<form onSubmit={props.onSubmit}>
|
||||
<div className="py-3">
|
||||
{!!props.profileDataManager && (
|
||||
<ManagedProfileAlert profileDataManager={props.profileDataManager} />
|
||||
)}
|
||||
<ErrorMessage message={props.formErrors.full_name} />
|
||||
<label className="h6" htmlFor="fullName">
|
||||
{props.intl.formatMessage(messages['account.settings.coaching.consent.label.name'])}
|
||||
</label>
|
||||
<Form.Control
|
||||
type="text"
|
||||
name="full-name"
|
||||
id="fullName"
|
||||
disabled={!!props.profileDataManager}
|
||||
defaultValue={props.formValues.name}
|
||||
/>
|
||||
</div>
|
||||
<div className="py-3">
|
||||
<ErrorMessage message={props.formErrors.phone_number} />
|
||||
<label className="h6" htmlFor="phoneNumber">
|
||||
{props.intl.formatMessage(messages['account.settings.coaching.consent.label.phone-number'])}
|
||||
</label>
|
||||
<Form.Control
|
||||
type="text"
|
||||
name="phone_number"
|
||||
id="phoneNumber"
|
||||
defaultValue={props.formValues.phone_number}
|
||||
/>
|
||||
</div>
|
||||
<div className=" py-3">
|
||||
<p className="small font-italic">
|
||||
{props.intl.formatMessage(messages['account.settings.coaching.consent.text-messaging.disclaimer'])}
|
||||
</p>
|
||||
</div>
|
||||
<ErrorMessage message={props.formErrors.coaching} />
|
||||
<div className="d-flex flex-column align-items-center">
|
||||
<Button variant="outline-primary" className="w-100" type="submit">
|
||||
{props.intl.formatMessage(messages['account.settings.coaching.consent.accept-coaching'])}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<Hyperlink
|
||||
className="mt-3 text-dark btn-link small"
|
||||
destination={props.redirectUrl}
|
||||
onClick={props.declineCoaching}
|
||||
>
|
||||
{props.intl.formatMessage(messages['account.settings.coaching.consent.decline-coaching'])}
|
||||
</Hyperlink>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
CoachingForm.defaultProps = {
|
||||
formErrors: {
|
||||
coaching: '',
|
||||
name: '',
|
||||
phone_number: '',
|
||||
},
|
||||
};
|
||||
|
||||
CoachingForm.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
declineCoaching: PropTypes.func.isRequired,
|
||||
formValues: PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
phone_number: PropTypes.string,
|
||||
coaching: PropTypes.shape({
|
||||
coaching_consent: PropTypes.bool.isRequired,
|
||||
user: PropTypes.number.isRequired,
|
||||
eligible_for_coaching: PropTypes.bool.isRequired,
|
||||
consent_form_seen: PropTypes.bool.isRequired,
|
||||
}),
|
||||
}).isRequired,
|
||||
formErrors: PropTypes.shape({
|
||||
coaching: PropTypes.string,
|
||||
full_name: PropTypes.string,
|
||||
phone_number: PropTypes.string,
|
||||
}),
|
||||
redirectUrl: PropTypes.string.isRequired,
|
||||
profileDataManager: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
ErrorMessage.defaultProps = {
|
||||
message: '',
|
||||
};
|
||||
|
||||
ErrorMessage.propTypes = {
|
||||
message: PropTypes.string,
|
||||
};
|
||||
|
||||
ManagedProfileAlert.propTypes = {
|
||||
profileDataManager: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(CoachingForm);
|
||||
@@ -1,103 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Form } from '@edx/paragon';
|
||||
import messages from './CoachingToggle.messages';
|
||||
import { editableFieldSelector } from '../data/selectors';
|
||||
import { saveSettings, updateDraft, saveMultipleSettings } from '../data/actions';
|
||||
import EditableField from '../EditableField';
|
||||
|
||||
const CoachingToggle = (props) => (
|
||||
<>
|
||||
<EditableField
|
||||
name="phone_number"
|
||||
type="text"
|
||||
value={props.phone_number}
|
||||
label={props.intl.formatMessage(messages['account.settings.field.phone_number'])}
|
||||
emptyLabel={props.intl.formatMessage(messages['account.settings.field.phone_number.empty'])}
|
||||
onChange={props.updateDraft}
|
||||
onSubmit={() => {
|
||||
const { coaching } = props;
|
||||
if (coaching.coaching_consent === true) {
|
||||
return props.saveMultipleSettings([
|
||||
{
|
||||
formId: 'coaching',
|
||||
commitValues: {
|
||||
...coaching,
|
||||
phone_number: props.phone_number,
|
||||
},
|
||||
},
|
||||
{
|
||||
formId: 'phone_number',
|
||||
commitValues: props.phone_number,
|
||||
},
|
||||
], 'phone_number');
|
||||
}
|
||||
return props.saveSettings('phone_number', props.phone_number);
|
||||
}}
|
||||
/>
|
||||
<Form.Group
|
||||
isInvalid={!!props.error}
|
||||
className="custom-control custom-switch"
|
||||
>
|
||||
<Form.Switch
|
||||
name={props.name}
|
||||
className="custom-control-input"
|
||||
disabled={props.saveState === 'pending'}
|
||||
type="checkbox"
|
||||
id="coachingConsent"
|
||||
checked={props.coaching.coaching_consent}
|
||||
helperText={props.intl.formatMessage(messages['account.settings.field.coaching_consent.tooltip'])}
|
||||
value={props.coaching.coaching_consent}
|
||||
onChange={async (e) => {
|
||||
const { name } = e.target;
|
||||
// eslint-disable-next-line camelcase
|
||||
const { user, eligible_for_coaching } = props.coaching;
|
||||
const value = {
|
||||
user,
|
||||
// eslint-disable-next-line camelcase
|
||||
eligible_for_coaching,
|
||||
coaching_consent: e.target.checked,
|
||||
};
|
||||
props.saveSettings(name, value);
|
||||
}}
|
||||
>
|
||||
{props.intl.formatMessage(messages['account.settings.field.coaching_consent'])}
|
||||
</Form.Switch>
|
||||
{!!props.error && (
|
||||
<Form.Control.Feedback>
|
||||
{props.intl.formatMessage(messages['account.settings.field.coaching_consent.error'])}
|
||||
</Form.Control.Feedback>
|
||||
)}
|
||||
</Form.Group>
|
||||
</>
|
||||
);
|
||||
|
||||
CoachingToggle.defaultProps = {
|
||||
phone_number: '',
|
||||
error: '',
|
||||
saveState: undefined,
|
||||
};
|
||||
|
||||
CoachingToggle.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
error: PropTypes.string,
|
||||
coaching: PropTypes.shape({
|
||||
coaching_consent: PropTypes.bool.isRequired,
|
||||
user: PropTypes.number.isRequired,
|
||||
eligible_for_coaching: PropTypes.bool.isRequired,
|
||||
}).isRequired,
|
||||
saveState: PropTypes.oneOf(['default', 'pending', 'complete', 'error']),
|
||||
saveSettings: PropTypes.func.isRequired,
|
||||
saveMultipleSettings: PropTypes.func.isRequired,
|
||||
updateDraft: PropTypes.func.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
phone_number: PropTypes.string,
|
||||
};
|
||||
|
||||
export default connect(editableFieldSelector, {
|
||||
saveSettings,
|
||||
updateDraft,
|
||||
saveMultipleSettings,
|
||||
})(injectIntl(CoachingToggle));
|
||||
@@ -1,31 +0,0 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
'account.settings.field.phone_number': {
|
||||
id: 'account.settings.field.phone_number',
|
||||
defaultMessage: 'Phone Number',
|
||||
description: 'The label for a phone numbers setting in the user profile',
|
||||
},
|
||||
'account.settings.field.phone_number.empty': {
|
||||
id: 'account.settings.field.phone_number.empty',
|
||||
defaultMessage: 'Add a phone number',
|
||||
description: 'placeholder for a profiles empty phone number field',
|
||||
},
|
||||
'account.settings.field.coaching_consent': {
|
||||
id: 'account.settings.field.coaching_consent',
|
||||
defaultMessage: 'Coaching consent',
|
||||
description: 'The label for the coaching consent setting in the user profile',
|
||||
},
|
||||
'account.settings.field.coaching_consent.tooltip': {
|
||||
id: 'account.settings.field.coaching_consent.tooltip',
|
||||
defaultMessage: 'MicroBachelors programs include text message based coaching that helps you pair educational experiences with your career goals through one-on-one advice. Coaching services are included at no additional cost, and are available to learners with U.S. mobile phone numbers. Standard messaging rates apply. Text ‘STOP’ at anytime to opt-out of messages.',
|
||||
description: 'A tooltip explaining what coaching is and who it is for',
|
||||
},
|
||||
'account.settings.field.coaching_consent.error': {
|
||||
id: 'account.settings.field.coaching_consent.error',
|
||||
defaultMessage: 'A valid US phone number is required to opt into coaching',
|
||||
description: 'An error message that displays when a user attempts to consent to coaching without first providing a phone number in their profile',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -1,51 +0,0 @@
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import get from 'lodash.get';
|
||||
|
||||
/**
|
||||
* get all settings related to the coaching plugin. Settings used
|
||||
* by Microbachelors students.
|
||||
* @param {Number} userId users are identified in the api by LMS id
|
||||
*/
|
||||
export async function getCoachingPreferences(userId) {
|
||||
let data = {};
|
||||
try {
|
||||
({ data } = await getAuthenticatedHttpClient()
|
||||
.get(`${getConfig().LMS_BASE_URL}/api/coaching/v1/users/${userId}/`));
|
||||
} catch (error) {
|
||||
// If a user isn't active the API call will fail with a lack of credentials.
|
||||
data = {
|
||||
coaching_consent: false,
|
||||
user: userId,
|
||||
eligible_for_coaching: false,
|
||||
consent_form_seen: false,
|
||||
};
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* patch all of the settings related to coaching.
|
||||
* @param {Number} userId users are identified in the api by LMS id
|
||||
* @param {Object} commitValues { coaching }
|
||||
*/
|
||||
export async function patchCoachingPreferences(userId, commitValues) {
|
||||
const requestUrl = `${getConfig().LMS_BASE_URL}/api/coaching/v1/users/${userId}/`;
|
||||
const { coaching } = commitValues;
|
||||
coaching.user = userId;
|
||||
|
||||
await getAuthenticatedHttpClient()
|
||||
.patch(requestUrl, coaching)
|
||||
.catch((error) => {
|
||||
const apiError = Object.create(error);
|
||||
apiError.fieldErrors = JSON.parse(error.customAttributes.httpErrorResponseData);
|
||||
if (get(apiError, 'fieldErrors.phone_number')) {
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
apiError.fieldErrors.coaching = apiError.fieldErrors.phone_number[0];
|
||||
delete apiError.fieldErrors.phone_number;
|
||||
}
|
||||
throw apiError;
|
||||
});
|
||||
return commitValues;
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
/* eslint-disable no-import-assign */
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import renderer from 'react-test-renderer';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import * as auth from '@edx/frontend-platform/auth';
|
||||
|
||||
import CoachingConsent from '../CoachingConsent';
|
||||
import * as selectors from '../../data/selectors';
|
||||
|
||||
jest.mock('@edx/frontend-platform/auth');
|
||||
|
||||
const IntlCoachingConsent = injectIntl(CoachingConsent);
|
||||
|
||||
jest.mock('../../data/selectors', () => jest.fn().mockImplementation(() => ({ coachingConsentPageSelector: () => ({}) })));
|
||||
|
||||
const mockStore = configureStore();
|
||||
|
||||
describe('CoachingConsent', () => {
|
||||
let props = {};
|
||||
let store = {};
|
||||
selectors.mockClear();
|
||||
|
||||
const reduxWrapper = children => (
|
||||
<IntlProvider locale="en">
|
||||
<Provider store={store}>{children}</Provider>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
store = mockStore();
|
||||
props = {
|
||||
fetchSettings: jest.fn(),
|
||||
loaded: true,
|
||||
saveState: undefined,
|
||||
formValues: {
|
||||
name: 'edx edx',
|
||||
phone_number: '1234567890',
|
||||
coaching: {
|
||||
coaching_consent: true,
|
||||
consent_form_seen: false,
|
||||
eligible_for_coaching: true,
|
||||
user: 1,
|
||||
},
|
||||
},
|
||||
formErrors: {},
|
||||
confirmationValues: {},
|
||||
profileDataManager: '',
|
||||
intl: {},
|
||||
};
|
||||
auth.getAuthenticatedHttpClient = jest.fn(() => ({
|
||||
patch: async () => ({
|
||||
data: { status: 200 },
|
||||
catch: () => {},
|
||||
}),
|
||||
}));
|
||||
auth.getAuthenticatedUser = jest.fn(() => ({ userId: 3 }));
|
||||
});
|
||||
|
||||
it('should render', () => {
|
||||
const wrapper = renderer.create(reduxWrapper(<IntlCoachingConsent {...props} />)).toJSON();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('disables name field on enterprise user', () => {
|
||||
props = {
|
||||
...props,
|
||||
profileDataManager: 'test person',
|
||||
};
|
||||
const wrapper = renderer.create(reduxWrapper(<IntlCoachingConsent {...props} />)).toJSON();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('display completed box when successfully submitted', async () => {
|
||||
const fakeEvent = {
|
||||
preventDefault: () => {},
|
||||
target: {
|
||||
fullName: { value: 'edx edx' },
|
||||
phoneNumber: { value: '9783028731' },
|
||||
},
|
||||
};
|
||||
const wrapper = renderer.create(
|
||||
reduxWrapper(<IntlCoachingConsent {...props} />),
|
||||
{
|
||||
// bypass the forward-ref. we don't care about focus for this one test
|
||||
createNodeMock: (element) => {
|
||||
if (element.type === 'button') {
|
||||
// mock a focus function
|
||||
return {
|
||||
focus: async () => wrapper.root.findByType('form').props.onSubmit(fakeEvent),
|
||||
};
|
||||
}
|
||||
return null;
|
||||
},
|
||||
},
|
||||
);
|
||||
const form = wrapper.root.findByType('form');
|
||||
await act(async () => { await form.props.onSubmit(fakeEvent); });
|
||||
expect(wrapper.toJSON()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -1,300 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CoachingConsent disables name field on enterprise user 1`] = `
|
||||
<main>
|
||||
<div
|
||||
className="w-100 d-flex justify-content-center align-items-center shadow coaching-header"
|
||||
>
|
||||
<img
|
||||
alt="Logo"
|
||||
className="logo"
|
||||
src="icon/mock/path"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="col-12 col-md-6 col-xl-5 mx-auto mt-4 p-5 shadow-lg"
|
||||
>
|
||||
<h2
|
||||
className="h2"
|
||||
>
|
||||
Let’s get started.
|
||||
</h2>
|
||||
<p>
|
||||
MicroBachelors programs include coaching that focuses on your career, education, and how you'll achieve results through one-on-one communication with an experienced professional. If you’re interested, provide the information below and click “Submit,” and our coaching partner will connect with you via email and/or text message to help you move forward. Terms and conditions apply.*
|
||||
</p>
|
||||
<div>
|
||||
<form
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<div
|
||||
className="py-3"
|
||||
>
|
||||
<div
|
||||
className="alert d-flex align-items-start alert alert-primary"
|
||||
>
|
||||
<div />
|
||||
<div>
|
||||
Your name is managed by
|
||||
<b>
|
||||
test person
|
||||
</b>
|
||||
. Contact your administrator for help.
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="alert-warning mb-2"
|
||||
>
|
||||
|
||||
</div>
|
||||
<label
|
||||
className="h6"
|
||||
htmlFor="fullName"
|
||||
>
|
||||
Please confirm your name
|
||||
</label>
|
||||
<div
|
||||
className="pgn__form-control-decorator-group"
|
||||
>
|
||||
<input
|
||||
className="has-value form-control"
|
||||
defaultValue="edx edx"
|
||||
disabled={true}
|
||||
id="fullName"
|
||||
name="full-name"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="py-3"
|
||||
>
|
||||
<div
|
||||
className="alert-warning mb-2"
|
||||
>
|
||||
|
||||
</div>
|
||||
<label
|
||||
className="h6"
|
||||
htmlFor="phoneNumber"
|
||||
>
|
||||
Enter your mobile number
|
||||
</label>
|
||||
<div
|
||||
className="pgn__form-control-decorator-group"
|
||||
>
|
||||
<input
|
||||
className="has-value form-control"
|
||||
defaultValue="1234567890"
|
||||
id="phoneNumber"
|
||||
name="phone_number"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className=" py-3"
|
||||
>
|
||||
<p
|
||||
className="small font-italic"
|
||||
>
|
||||
* Coaching services are included at no additional cost to learners with US phone numbers. Coaching includes recurring text messages. Message and data rates may apply. Text STOP to opt-out.
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className="alert-warning mb-2"
|
||||
>
|
||||
|
||||
</div>
|
||||
<div
|
||||
className="d-flex flex-column align-items-center"
|
||||
>
|
||||
<button
|
||||
className="w-100 btn btn-outline-primary"
|
||||
disabled={false}
|
||||
type="submit"
|
||||
>
|
||||
Sign up for coaching
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
className="mt-3"
|
||||
>
|
||||
<a
|
||||
className="pgn__hyperlink default-link standalone-link mt-3 text-dark btn-link small"
|
||||
href="http://localhost:18000/dashboard/"
|
||||
onClick={[Function]}
|
||||
target="_self"
|
||||
>
|
||||
I prefer not to be contacted with free coaching services
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
`;
|
||||
|
||||
exports[`CoachingConsent display completed box when successfully submitted 1`] = `
|
||||
<main>
|
||||
<div
|
||||
className="w-100 d-flex justify-content-center align-items-center shadow coaching-header"
|
||||
>
|
||||
<img
|
||||
alt="Logo"
|
||||
className="logo"
|
||||
src="icon/mock/path"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
className="d-flex justify-content-center align-items-center flex-column"
|
||||
style={
|
||||
Object {
|
||||
"height": "50vh",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="spinner-border text-primary"
|
||||
role="status"
|
||||
>
|
||||
<span
|
||||
className="sr-only"
|
||||
>
|
||||
Submitting...
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
`;
|
||||
|
||||
exports[`CoachingConsent should render 1`] = `
|
||||
<main>
|
||||
<div
|
||||
className="w-100 d-flex justify-content-center align-items-center shadow coaching-header"
|
||||
>
|
||||
<img
|
||||
alt="Logo"
|
||||
className="logo"
|
||||
src="icon/mock/path"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="col-12 col-md-6 col-xl-5 mx-auto mt-4 p-5 shadow-lg"
|
||||
>
|
||||
<h2
|
||||
className="h2"
|
||||
>
|
||||
Let’s get started.
|
||||
</h2>
|
||||
<p>
|
||||
MicroBachelors programs include coaching that focuses on your career, education, and how you'll achieve results through one-on-one communication with an experienced professional. If you’re interested, provide the information below and click “Submit,” and our coaching partner will connect with you via email and/or text message to help you move forward. Terms and conditions apply.*
|
||||
</p>
|
||||
<div>
|
||||
<form
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<div
|
||||
className="py-3"
|
||||
>
|
||||
<div
|
||||
className="alert-warning mb-2"
|
||||
>
|
||||
|
||||
</div>
|
||||
<label
|
||||
className="h6"
|
||||
htmlFor="fullName"
|
||||
>
|
||||
Please confirm your name
|
||||
</label>
|
||||
<div
|
||||
className="pgn__form-control-decorator-group"
|
||||
>
|
||||
<input
|
||||
className="has-value form-control"
|
||||
defaultValue="edx edx"
|
||||
disabled={false}
|
||||
id="fullName"
|
||||
name="full-name"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="py-3"
|
||||
>
|
||||
<div
|
||||
className="alert-warning mb-2"
|
||||
>
|
||||
|
||||
</div>
|
||||
<label
|
||||
className="h6"
|
||||
htmlFor="phoneNumber"
|
||||
>
|
||||
Enter your mobile number
|
||||
</label>
|
||||
<div
|
||||
className="pgn__form-control-decorator-group"
|
||||
>
|
||||
<input
|
||||
className="has-value form-control"
|
||||
defaultValue="1234567890"
|
||||
id="phoneNumber"
|
||||
name="phone_number"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className=" py-3"
|
||||
>
|
||||
<p
|
||||
className="small font-italic"
|
||||
>
|
||||
* Coaching services are included at no additional cost to learners with US phone numbers. Coaching includes recurring text messages. Message and data rates may apply. Text STOP to opt-out.
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className="alert-warning mb-2"
|
||||
>
|
||||
|
||||
</div>
|
||||
<div
|
||||
className="d-flex flex-column align-items-center"
|
||||
>
|
||||
<button
|
||||
className="w-100 btn btn-outline-primary"
|
||||
disabled={false}
|
||||
type="submit"
|
||||
>
|
||||
Sign up for coaching
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
className="mt-3"
|
||||
>
|
||||
<a
|
||||
className="pgn__hyperlink default-link standalone-link mt-3 text-dark btn-link small"
|
||||
href="http://localhost:18000/dashboard/"
|
||||
onClick={[Function]}
|
||||
target="_self"
|
||||
>
|
||||
I prefer not to be contacted with free coaching services
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
`;
|
||||
@@ -27,6 +27,7 @@ export const fetchSettingsSuccess = ({
|
||||
profileDataManager,
|
||||
timeZones,
|
||||
verifiedNameHistory,
|
||||
countriesCodesList,
|
||||
}) => ({
|
||||
type: FETCH_SETTINGS.SUCCESS,
|
||||
payload: {
|
||||
@@ -35,6 +36,7 @@ export const fetchSettingsSuccess = ({
|
||||
profileDataManager,
|
||||
timeZones,
|
||||
verifiedNameHistory,
|
||||
countriesCodesList,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -77,9 +79,9 @@ export const beginNameChange = (formId) => ({
|
||||
});
|
||||
// SAVE SETTINGS ACTIONS
|
||||
|
||||
export const saveSettings = (formId, commitValues) => ({
|
||||
export const saveSettings = (formId, commitValues, extendedProfile = {}) => ({
|
||||
type: SAVE_SETTINGS.BASE,
|
||||
payload: { formId, commitValues },
|
||||
payload: { formId, commitValues, extendedProfile },
|
||||
});
|
||||
|
||||
export const saveSettingsBegin = () => ({
|
||||
|
||||
@@ -25,7 +25,7 @@ export const EDUCATION_LEVELS = [
|
||||
'jhs',
|
||||
'el',
|
||||
'none',
|
||||
'o',
|
||||
'other',
|
||||
];
|
||||
|
||||
export const GENDER_OPTIONS = [
|
||||
@@ -34,6 +34,21 @@ export const GENDER_OPTIONS = [
|
||||
'm',
|
||||
'o',
|
||||
];
|
||||
export const WORK_EXPERIENCE_OPTIONS = [
|
||||
'',
|
||||
'0',
|
||||
'1',
|
||||
'2',
|
||||
'3',
|
||||
'4',
|
||||
'5',
|
||||
'6',
|
||||
'7',
|
||||
'8',
|
||||
'9',
|
||||
'10+',
|
||||
|
||||
];
|
||||
|
||||
export const COUNTRY_WITH_STATES = 'US';
|
||||
|
||||
@@ -117,6 +132,6 @@ export function getStatesList(country) {
|
||||
return country && COUNTRY_STATES_MAP[country.toUpperCase()];
|
||||
}
|
||||
|
||||
export const DECLINED = 'declined';
|
||||
export const SELF_DESCRIBE = 'self-describe';
|
||||
export const OTHER = 'other';
|
||||
export const FIELD_LABELS = {
|
||||
COUNTRY: 'country',
|
||||
};
|
||||
|
||||
@@ -39,6 +39,7 @@ export const defaultState = {
|
||||
verifiedName: null,
|
||||
mostRecentVerifiedName: {},
|
||||
verifiedNameHistory: {},
|
||||
countriesCodesList: [],
|
||||
};
|
||||
|
||||
const reducer = (state = defaultState, action = {}) => {
|
||||
@@ -64,6 +65,7 @@ const reducer = (state = defaultState, action = {}) => {
|
||||
loaded: true,
|
||||
loadingError: null,
|
||||
verifiedNameHistory: action.payload.verifiedNameHistory,
|
||||
countriesCodesList: action.payload.countriesCodesList,
|
||||
};
|
||||
case FETCH_SETTINGS.FAILURE:
|
||||
return {
|
||||
|
||||
@@ -53,7 +53,7 @@ export function* handleFetchSettings() {
|
||||
const { username, userId, roles: userRoles } = getAuthenticatedUser();
|
||||
|
||||
const {
|
||||
thirdPartyAuthProviders, profileDataManager, timeZones, ...values
|
||||
thirdPartyAuthProviders, profileDataManager, timeZones, countries, ...values
|
||||
} = yield call(
|
||||
getSettings,
|
||||
username,
|
||||
@@ -71,6 +71,7 @@ export function* handleFetchSettings() {
|
||||
profileDataManager,
|
||||
timeZones,
|
||||
verifiedNameHistory,
|
||||
countriesCodesList: countries,
|
||||
}));
|
||||
} catch (e) {
|
||||
yield put(fetchSettingsFailure(e.message));
|
||||
@@ -83,8 +84,8 @@ export function* handleSaveSettings(action) {
|
||||
yield put(saveSettingsBegin());
|
||||
|
||||
const { username, userId } = getAuthenticatedUser();
|
||||
const { commitValues, formId } = action.payload;
|
||||
const commitData = { [formId]: commitValues };
|
||||
const { commitValues, formId, extendedProfile } = action.payload;
|
||||
const commitData = Object.keys(extendedProfile).length > 0 ? extendedProfile : { [formId]: commitValues };
|
||||
let savedValues = null;
|
||||
if (formId === 'siteLanguage') {
|
||||
const previousSiteLanguage = getLocale();
|
||||
|
||||
@@ -88,6 +88,11 @@ const previousSiteLanguageSelector = createSelector(
|
||||
accountSettings => accountSettings.previousSiteLanguage,
|
||||
);
|
||||
|
||||
const countriesSelector = createSelector(
|
||||
accountSettingsSelector,
|
||||
accountSettings => accountSettings.countriesCodesList,
|
||||
);
|
||||
|
||||
const editableFieldErrorSelector = createSelector(
|
||||
editableFieldNameSelector,
|
||||
accountSettingsSelector,
|
||||
@@ -106,11 +111,6 @@ const isEditingSelector = createSelector(
|
||||
(name, accountSettings) => accountSettings.openFormId === name,
|
||||
);
|
||||
|
||||
const confirmationValuesSelector = createSelector(
|
||||
accountSettingsSelector,
|
||||
accountSettings => accountSettings.confirmationValues,
|
||||
);
|
||||
|
||||
const errorSelector = createSelector(
|
||||
accountSettingsSelector,
|
||||
accountSettings => accountSettings.errors,
|
||||
@@ -161,7 +161,7 @@ function chooseFormValue(draft, committed) {
|
||||
return draft !== undefined ? draft : committed;
|
||||
}
|
||||
|
||||
const formValuesSelector = createSelector(
|
||||
export const formValuesSelector = createSelector(
|
||||
valuesSelector,
|
||||
draftsSelector,
|
||||
(values, drafts) => {
|
||||
@@ -169,6 +169,20 @@ const formValuesSelector = createSelector(
|
||||
Object.entries(values).forEach(([name, value]) => {
|
||||
if (typeof value === 'boolean') {
|
||||
formValues[name] = chooseFormValue(drafts[name], value);
|
||||
} else if (typeof value === 'object' && name === 'extended_profile' && value !== null) {
|
||||
const extendedProfile = value.slice();
|
||||
const draftsKeys = Object.keys(drafts);
|
||||
|
||||
if (draftsKeys.length !== 0) {
|
||||
const draftFieldName = draftsKeys[0];
|
||||
const index = extendedProfile.findIndex((profile) => profile.field_name === draftFieldName);
|
||||
|
||||
if (index !== -1) {
|
||||
extendedProfile[index] = { field_name: draftFieldName, field_value: drafts[draftFieldName] };
|
||||
}
|
||||
}
|
||||
|
||||
formValues.extended_profile = [...extendedProfile];
|
||||
} else {
|
||||
formValues[name] = chooseFormValue(drafts[name], value) || '';
|
||||
}
|
||||
@@ -228,6 +242,7 @@ export const accountSettingsPageSelector = createSelector(
|
||||
mostRecentApprovedVerifiedNameValueSelector,
|
||||
mostRecentVerifiedNameSelector,
|
||||
sortedVerifiedNameHistorySelector,
|
||||
countriesSelector,
|
||||
(
|
||||
accountSettings,
|
||||
siteLanguageOptions,
|
||||
@@ -245,6 +260,7 @@ export const accountSettingsPageSelector = createSelector(
|
||||
verifiedName,
|
||||
mostRecentVerifiedName,
|
||||
verifiedNameHistory,
|
||||
countriesCodesList,
|
||||
) => ({
|
||||
siteLanguageOptions,
|
||||
siteLanguage,
|
||||
@@ -265,6 +281,7 @@ export const accountSettingsPageSelector = createSelector(
|
||||
verifiedName,
|
||||
mostRecentVerifiedName,
|
||||
verifiedNameHistory,
|
||||
countriesCodesList,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -289,50 +306,6 @@ export const certPreferenceSelector = createSelector(
|
||||
}),
|
||||
);
|
||||
|
||||
export const coachingConsentPageSelector = createSelector(
|
||||
accountSettingsSelector,
|
||||
formValuesSelector,
|
||||
activeAccountSelector,
|
||||
profileDataManagerSelector,
|
||||
saveStateSelector,
|
||||
confirmationValuesSelector,
|
||||
errorSelector,
|
||||
(
|
||||
accountSettings,
|
||||
formValues,
|
||||
activeAccount,
|
||||
profileDataManager,
|
||||
saveState,
|
||||
confirmationValues,
|
||||
errors,
|
||||
) => ({
|
||||
loading: accountSettings.loading,
|
||||
loaded: accountSettings.loaded,
|
||||
loadingError: accountSettings.loadingError,
|
||||
isActive: activeAccount,
|
||||
profileDataManager,
|
||||
formValues,
|
||||
saveState,
|
||||
confirmationValues,
|
||||
formErrors: errors,
|
||||
}),
|
||||
);
|
||||
|
||||
export const demographicsSectionSelector = createSelector(
|
||||
formValuesSelector,
|
||||
draftsSelector,
|
||||
errorSelector,
|
||||
(
|
||||
formValues,
|
||||
drafts,
|
||||
errors,
|
||||
) => ({
|
||||
formValues,
|
||||
drafts,
|
||||
formErrors: errors,
|
||||
}),
|
||||
);
|
||||
|
||||
export const nameChangeSelector = createSelector(
|
||||
accountSettingsSelector,
|
||||
formValuesSelector,
|
||||
|
||||
72
src/account-settings/data/selectors.test.js
Normal file
72
src/account-settings/data/selectors.test.js
Normal file
@@ -0,0 +1,72 @@
|
||||
import { profileDataManagerSelector, formValuesSelector } from './selectors';
|
||||
|
||||
const testValue = 'test VALUE';
|
||||
|
||||
describe('profileDataManagerSelector', () => {
|
||||
it('returns the profileDataManager from the state', () => {
|
||||
const state = {
|
||||
accountSettings: {
|
||||
profileDataManager: { testValue },
|
||||
},
|
||||
};
|
||||
const result = profileDataManagerSelector(state);
|
||||
|
||||
expect(result).toEqual(state.accountSettings.profileDataManager);
|
||||
});
|
||||
|
||||
it('should correctly select form values', () => {
|
||||
const state = {
|
||||
accountSettings: {
|
||||
values: {
|
||||
name: 'John Doe',
|
||||
age: 25,
|
||||
},
|
||||
drafts: {
|
||||
age: 26,
|
||||
|
||||
},
|
||||
verifiedNameHistory: 'test',
|
||||
confirmationValues: {},
|
||||
},
|
||||
};
|
||||
|
||||
const result = formValuesSelector(state);
|
||||
|
||||
const expected = {
|
||||
name: 'John Doe',
|
||||
age: 26,
|
||||
verified_name: '',
|
||||
useVerifiedNameForCerts: false,
|
||||
};
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should correctly select form values with extended_profile', () => {
|
||||
// Mock data with extended_profile field in both values and drafts
|
||||
const state = {
|
||||
accountSettings: {
|
||||
values: {
|
||||
extended_profile: [
|
||||
{ field_name: 'test_field', field_value: '5' },
|
||||
],
|
||||
},
|
||||
drafts: { test_field: '6' },
|
||||
verifiedNameHistory: 'test',
|
||||
confirmationValues: {},
|
||||
},
|
||||
};
|
||||
|
||||
const result = formValuesSelector(state);
|
||||
|
||||
const expected = {
|
||||
verified_name: '',
|
||||
useVerifiedNameForCerts: false,
|
||||
extended_profile: [ // Draft value should override the committed value
|
||||
{ field_name: 'test_field', field_value: '6' }, // Value from the committed values
|
||||
],
|
||||
};
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
});
|
||||
@@ -1,16 +1,14 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { logError } from '@edx/frontend-platform/logging';
|
||||
import pick from 'lodash.pick';
|
||||
import pickBy from 'lodash.pickby';
|
||||
import omit from 'lodash.omit';
|
||||
import isEmpty from 'lodash.isempty';
|
||||
|
||||
import { handleRequestError, unpackFieldErrors } from './utils';
|
||||
import { getThirdPartyAuthProviders } from '../third-party-auth';
|
||||
import { postVerifiedNameConfig } from '../certificate-preference/data/service';
|
||||
import { getCoachingPreferences, patchCoachingPreferences } from '../coaching/data/service';
|
||||
import { getDemographics, getDemographicsOptions, patchDemographics } from '../demographics/data/service';
|
||||
import { DEMOGRAPHICS_FIELDS } from '../demographics/data/utils';
|
||||
import { FIELD_LABELS } from './constants';
|
||||
|
||||
const SOCIAL_PLATFORMS = [
|
||||
{ id: 'twitter', key: 'social_link_twitter' },
|
||||
@@ -155,28 +153,6 @@ export async function getProfileDataManager(username, userRoles) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* A function to determine if the Demographics questions should be displayed to the user. For the
|
||||
* MVP release of Demographics we are limiting the Demographics question visibility only to
|
||||
* MicroBachelors learners.
|
||||
*/
|
||||
export async function shouldDisplayDemographicsQuestions() {
|
||||
const requestUrl = `${getConfig().LMS_BASE_URL}/api/demographics/v1/demographics/status/`;
|
||||
let data = {};
|
||||
|
||||
try {
|
||||
({ data } = await getAuthenticatedHttpClient().get(requestUrl));
|
||||
if (data.display) {
|
||||
return data.display;
|
||||
}
|
||||
} catch (error) {
|
||||
// if there was an error then we just hide the section
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function getVerifiedName() {
|
||||
let data;
|
||||
const client = getAuthenticatedHttpClient();
|
||||
@@ -212,31 +188,43 @@ export async function postVerifiedName(data) {
|
||||
.catch(error => handleRequestError(error));
|
||||
}
|
||||
|
||||
function extractCountryList(data) {
|
||||
return data?.fields
|
||||
.find(({ name }) => name === FIELD_LABELS.COUNTRY)
|
||||
?.options?.map(({ value }) => (value)) || [];
|
||||
}
|
||||
|
||||
export async function getCountryList() {
|
||||
const url = `${getConfig().LMS_BASE_URL}/user_api/v1/account/registration/`;
|
||||
|
||||
try {
|
||||
const { data } = await getAuthenticatedHttpClient().get(url);
|
||||
return extractCountryList(data);
|
||||
} catch (e) {
|
||||
logError(e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A single function to GET everything considered a setting.
|
||||
* Currently encapsulates Account, Preferences, Coaching, ThirdPartyAuth, and Demographics
|
||||
* A single function to GET everything considered a setting. Currently encapsulates Account, Preferences, and
|
||||
* ThirdPartyAuth.
|
||||
*/
|
||||
export async function getSettings(username, userRoles, userId) {
|
||||
export async function getSettings(username, userRoles) {
|
||||
const [
|
||||
account,
|
||||
preferences,
|
||||
thirdPartyAuthProviders,
|
||||
profileDataManager,
|
||||
timeZones,
|
||||
coaching,
|
||||
shouldDisplayDemographicsQuestionsResponse,
|
||||
demographics,
|
||||
demographicsOptions,
|
||||
countries,
|
||||
] = await Promise.all([
|
||||
getAccount(username),
|
||||
getPreferences(username),
|
||||
getThirdPartyAuthProviders(),
|
||||
getProfileDataManager(username, userRoles),
|
||||
getTimeZones(),
|
||||
getConfig().COACHING_ENABLED && getCoachingPreferences(userId),
|
||||
getConfig().ENABLE_DEMOGRAPHICS_COLLECTION && shouldDisplayDemographicsQuestions(),
|
||||
getConfig().ENABLE_DEMOGRAPHICS_COLLECTION && getDemographics(userId),
|
||||
getConfig().ENABLE_DEMOGRAPHICS_COLLECTION && getDemographicsOptions(),
|
||||
getCountryList(),
|
||||
]);
|
||||
|
||||
return {
|
||||
@@ -245,36 +233,26 @@ export async function getSettings(username, userRoles, userId) {
|
||||
thirdPartyAuthProviders,
|
||||
profileDataManager,
|
||||
timeZones,
|
||||
coaching,
|
||||
shouldDisplayDemographicsSection: shouldDisplayDemographicsQuestionsResponse,
|
||||
...demographics,
|
||||
demographicsOptions,
|
||||
countries,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* A single function to PATCH everything considered a setting.
|
||||
* Currently encapsulates Account, Preferences, coaching and ThirdPartyAuth
|
||||
* Currently encapsulates Account, Preferences, ThirdPartyAuth
|
||||
*/
|
||||
export async function patchSettings(username, commitValues, userId) {
|
||||
export async function patchSettings(username, commitValues) {
|
||||
// Note: time_zone exists in the return value from user/v1/accounts
|
||||
// but it is always null and won't update. It also exists in
|
||||
// user/v1/preferences where it does update. This is the one we use.
|
||||
const preferenceKeys = ['time_zone'];
|
||||
const coachingKeys = ['coaching'];
|
||||
const demographicsKeys = DEMOGRAPHICS_FIELDS;
|
||||
const certificateKeys = ['useVerifiedNameForCerts'];
|
||||
const isDemographicsKey = (value, key) => key.includes('demographics');
|
||||
const accountCommitValues = omit(
|
||||
commitValues,
|
||||
preferenceKeys,
|
||||
coachingKeys,
|
||||
demographicsKeys,
|
||||
certificateKeys,
|
||||
);
|
||||
const preferenceCommitValues = pick(commitValues, preferenceKeys);
|
||||
const coachingCommitValues = pick(commitValues, coachingKeys);
|
||||
const demographicsCommitValues = pickBy(commitValues, isDemographicsKey);
|
||||
const certCommitValues = pick(commitValues, certificateKeys);
|
||||
const patchRequests = [];
|
||||
|
||||
@@ -284,12 +262,6 @@ export async function patchSettings(username, commitValues, userId) {
|
||||
if (!isEmpty(preferenceCommitValues)) {
|
||||
patchRequests.push(patchPreferences(username, preferenceCommitValues));
|
||||
}
|
||||
if (!isEmpty(coachingCommitValues)) {
|
||||
patchRequests.push(patchCoachingPreferences(userId, coachingCommitValues));
|
||||
}
|
||||
if (!isEmpty(demographicsCommitValues)) {
|
||||
patchRequests.push(patchDemographics(userId, demographicsCommitValues));
|
||||
}
|
||||
if (!isEmpty(certCommitValues)) {
|
||||
patchRequests.push(postVerifiedNameConfig(username, certCommitValues));
|
||||
}
|
||||
|
||||
181
src/account-settings/data/service.test.js
Normal file
181
src/account-settings/data/service.test.js
Normal file
@@ -0,0 +1,181 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { logError } from '@edx/frontend-platform/logging';
|
||||
import { FIELD_LABELS } from './constants';
|
||||
import {
|
||||
getAccount,
|
||||
patchAccount,
|
||||
getPreferences,
|
||||
patchPreferences,
|
||||
getTimeZones,
|
||||
getProfileDataManager,
|
||||
getVerifiedName,
|
||||
getVerifiedNameHistory,
|
||||
postVerifiedName,
|
||||
getCountryList,
|
||||
patchSettings,
|
||||
} from './service';
|
||||
|
||||
jest.mock('@edx/frontend-platform');
|
||||
jest.mock('@edx/frontend-platform/auth');
|
||||
jest.mock('@edx/frontend-platform/logging');
|
||||
|
||||
const mockHttpClient = {
|
||||
get: jest.fn(),
|
||||
patch: jest.fn(),
|
||||
post: jest.fn(),
|
||||
};
|
||||
|
||||
getAuthenticatedHttpClient.mockReturnValue(mockHttpClient);
|
||||
getConfig.mockReturnValue({ LMS_BASE_URL: 'http://lms.test' });
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('account service', () => {
|
||||
describe('getAccount', () => {
|
||||
it('returns unpacked account data', async () => {
|
||||
const apiResponse = {
|
||||
username: 'testuser',
|
||||
social_links: [{ platform: 'twitter', social_link: 'http://t' }],
|
||||
language_proficiencies: [{ code: 'en' }],
|
||||
};
|
||||
mockHttpClient.get.mockResolvedValue({ data: apiResponse });
|
||||
|
||||
const result = await getAccount('testuser');
|
||||
expect(mockHttpClient.get).toHaveBeenCalledWith('http://lms.test/api/user/v1/accounts/testuser');
|
||||
expect(result.social_link_twitter).toEqual('http://t');
|
||||
expect(result.language_proficiencies).toEqual('en');
|
||||
});
|
||||
});
|
||||
|
||||
describe('patchAccount', () => {
|
||||
it('sends packed commit data and returns unpacked response', async () => {
|
||||
const commit = { social_link_twitter: 'http://t' };
|
||||
const apiResponse = {
|
||||
username: 'testuser',
|
||||
social_links: [{ platform: 'twitter', social_link: 'http://t' }],
|
||||
language_proficiencies: [],
|
||||
};
|
||||
mockHttpClient.patch.mockResolvedValue({ data: apiResponse });
|
||||
|
||||
const result = await patchAccount('testuser', commit);
|
||||
expect(mockHttpClient.patch).toHaveBeenCalledWith(
|
||||
'http://lms.test/api/user/v1/accounts/testuser',
|
||||
expect.objectContaining({ social_links: [{ platform: 'twitter', social_link: 'http://t' }] }),
|
||||
expect.any(Object),
|
||||
);
|
||||
expect(result.social_link_twitter).toEqual('http://t');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPreferences', () => {
|
||||
it('returns preferences data', async () => {
|
||||
mockHttpClient.get.mockResolvedValue({ data: { theme: 'dark' } });
|
||||
const result = await getPreferences('user');
|
||||
expect(result.theme).toBe('dark');
|
||||
});
|
||||
});
|
||||
|
||||
describe('patchPreferences', () => {
|
||||
it('patches preferences and returns commitValues', async () => {
|
||||
mockHttpClient.patch.mockResolvedValue({});
|
||||
const commit = { time_zone: 'UTC' };
|
||||
const result = await patchPreferences('user', commit);
|
||||
expect(mockHttpClient.patch).toHaveBeenCalled();
|
||||
expect(result).toEqual(commit);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTimeZones', () => {
|
||||
it('returns data from API', async () => {
|
||||
mockHttpClient.get.mockResolvedValue({ data: ['UTC', 'PST'] });
|
||||
const result = await getTimeZones('PK');
|
||||
expect(mockHttpClient.get).toHaveBeenCalledWith(
|
||||
'http://lms.test/user_api/v1/preferences/time_zones/',
|
||||
{ params: { country_code: 'PK' } },
|
||||
);
|
||||
expect(result).toEqual(['UTC', 'PST']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getProfileDataManager', () => {
|
||||
it('returns null if no enterprise manages profile', async () => {
|
||||
mockHttpClient.get.mockResolvedValue({ data: { results: [] } });
|
||||
const result = await getProfileDataManager('user', ['learner']);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('returns enterprise name if sync is enabled', async () => {
|
||||
mockHttpClient.get.mockResolvedValue({ data: { results: [{ enterprise_customer: { name: 'Acme', sync_learner_profile_data: true } }] } });
|
||||
const result = await getProfileDataManager('user', ['enterprise_learner']);
|
||||
expect(result).toBe('Acme');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getVerifiedName', () => {
|
||||
it('returns verified name data', async () => {
|
||||
mockHttpClient.get.mockResolvedValue({ data: { verified: true } });
|
||||
const result = await getVerifiedName();
|
||||
expect(result.verified).toBe(true);
|
||||
});
|
||||
|
||||
it('returns {} on error', async () => {
|
||||
mockHttpClient.get.mockRejectedValue(new Error('fail'));
|
||||
const result = await getVerifiedName();
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getVerifiedNameHistory', () => {
|
||||
it('returns verified name history data', async () => {
|
||||
mockHttpClient.get.mockResolvedValue({ data: [{ id: 1 }] });
|
||||
const result = await getVerifiedNameHistory();
|
||||
expect(result[0].id).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('postVerifiedName', () => {
|
||||
it('posts verified name data', async () => {
|
||||
mockHttpClient.post.mockResolvedValue({});
|
||||
await postVerifiedName({ first_name: 'A' });
|
||||
expect(mockHttpClient.post).toHaveBeenCalledWith(
|
||||
'http://lms.test/api/edx_name_affirmation/v1/verified_name',
|
||||
{ first_name: 'A' },
|
||||
{ headers: { Accept: 'application/json' } },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCountryList', () => {
|
||||
it('extracts country values from registration API', async () => {
|
||||
const apiResponse = { fields: [{ name: FIELD_LABELS.COUNTRY, options: [{ value: 'PK' }] }] };
|
||||
mockHttpClient.get.mockResolvedValue({ data: apiResponse });
|
||||
const result = await getCountryList();
|
||||
expect(result).toEqual(['PK']);
|
||||
});
|
||||
|
||||
it('returns [] and logs error on failure', async () => {
|
||||
mockHttpClient.get.mockRejectedValue(new Error('fail'));
|
||||
const result = await getCountryList();
|
||||
expect(result).toEqual([]);
|
||||
expect(logError).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('patchSettings', () => {
|
||||
it('calls patchAccount and patchPreferences as needed', async () => {
|
||||
mockHttpClient.patch.mockResolvedValue({
|
||||
data: {
|
||||
username: 'user',
|
||||
social_links: [],
|
||||
language_proficiencies: [],
|
||||
},
|
||||
});
|
||||
|
||||
const result = await patchSettings('user', { time_zone: 'UTC', social_link_twitter: 't' });
|
||||
expect(result.username).toBe('user');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,9 +1,8 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { Hyperlink } from '@edx/paragon';
|
||||
import { Hyperlink } from '@openedx/paragon';
|
||||
|
||||
// Messages
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
@@ -13,7 +12,8 @@ import messages from './messages';
|
||||
import Alert from '../Alert';
|
||||
|
||||
const BeforeProceedingBanner = (props) => {
|
||||
const { instructionMessageId, intl, supportArticleUrl } = props;
|
||||
const { instructionMessageId, supportArticleUrl } = props;
|
||||
const intl = useIntl();
|
||||
|
||||
return (
|
||||
<Alert
|
||||
@@ -25,10 +25,12 @@ const BeforeProceedingBanner = (props) => {
|
||||
defaultMessage="Before proceeding, please {actionLink}."
|
||||
description="Error that appears if you are trying to delete your account, but something about your account needs attention first. The actionLink will be instructions, such as 'unlink your Facebook account'."
|
||||
values={{
|
||||
actionLink: (
|
||||
actionLink: supportArticleUrl ? (
|
||||
<Hyperlink destination={supportArticleUrl}>
|
||||
{intl.formatMessage(messages[instructionMessageId])}
|
||||
</Hyperlink>
|
||||
) : (
|
||||
intl.formatMessage(messages[instructionMessageId])
|
||||
),
|
||||
siteName: getConfig().SITE_NAME,
|
||||
}}
|
||||
@@ -39,8 +41,7 @@ const BeforeProceedingBanner = (props) => {
|
||||
|
||||
BeforeProceedingBanner.propTypes = {
|
||||
instructionMessageId: PropTypes.string.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
supportArticleUrl: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(BeforeProceedingBanner);
|
||||
export default BeforeProceedingBanner;
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import renderer from 'react-test-renderer';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
|
||||
jest.mock('react-dom', () => ({
|
||||
...jest.requireActual('react-dom'),
|
||||
createPortal: jest.fn(node => node), // Mock portal behavior
|
||||
}));
|
||||
|
||||
import BeforeProceedingBanner from './BeforeProceedingBanner'; // eslint-disable-line import/first
|
||||
|
||||
describe('BeforeProceedingBanner', () => {
|
||||
it('should match the snapshot if SUPPORT_URL_TO_UNLINK_SOCIAL_MEDIA_ACCOUNT does not have a support link', () => {
|
||||
const props = {
|
||||
instructionMessageId: 'account.settings.delete.account.please.unlink',
|
||||
supportArticleUrl: '',
|
||||
};
|
||||
const tree = renderer
|
||||
.create((
|
||||
<IntlProvider locale="en">
|
||||
<BeforeProceedingBanner
|
||||
{...props}
|
||||
/>
|
||||
</IntlProvider>
|
||||
))
|
||||
.toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should match the snapshot when SUPPORT_URL_TO_UNLINK_SOCIAL_MEDIA_ACCOUNT has a support link', () => {
|
||||
const props = {
|
||||
instructionMessageId: 'account.settings.delete.account.please.unlink',
|
||||
supportArticleUrl: 'http://test-support.edx',
|
||||
};
|
||||
const tree = renderer
|
||||
.create((
|
||||
<IntlProvider locale="en">
|
||||
<BeforeProceedingBanner
|
||||
{...props}
|
||||
/>
|
||||
</IntlProvider>
|
||||
))
|
||||
.toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -3,8 +3,8 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import {
|
||||
AlertModal,
|
||||
Button, Input, ValidationFormGroup, ActionRow,
|
||||
} from '@edx/paragon';
|
||||
Button, Form, ActionRow,
|
||||
} from '@openedx/paragon';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { faExclamationCircle, faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
@@ -78,6 +78,7 @@ export class ConfirmationModal extends Component {
|
||||
isOpen={open}
|
||||
title={intl.formatMessage(messages['account.settings.delete.account.modal.header'])}
|
||||
onClose={onCancel}
|
||||
isOverflowVisible
|
||||
footerNode={(
|
||||
<ActionRow>
|
||||
<Button variant="link" onClick={onCancel}>Cancel</Button>
|
||||
@@ -107,22 +108,26 @@ export class ConfirmationModal extends Component {
|
||||
<PrintingInstructions />
|
||||
</p>
|
||||
</Alert>
|
||||
<ValidationFormGroup
|
||||
<Form.Group
|
||||
for={passwordFieldId}
|
||||
invalid={errorType !== null}
|
||||
invalidMessage={intl.formatMessage(invalidMessage)}
|
||||
isInvalid={errorType !== null}
|
||||
>
|
||||
<label className="d-block" htmlFor={passwordFieldId}>
|
||||
<Form.Label className="d-block" htmlFor={passwordFieldId}>
|
||||
{intl.formatMessage(messages['account.settings.delete.account.modal.enter.password'])}
|
||||
</label>
|
||||
<Input
|
||||
</Form.Label>
|
||||
<Form.Control
|
||||
name="password"
|
||||
id={passwordFieldId}
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</ValidationFormGroup>
|
||||
{errorType !== null && (
|
||||
<Form.Control.Feedback type="invalid" feedback-for={passwordFieldId}>
|
||||
{intl.formatMessage(invalidMessage)}
|
||||
</Form.Control.Feedback>
|
||||
)}
|
||||
</Form.Group>
|
||||
</div>
|
||||
|
||||
</AlertModal>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import renderer from 'react-test-renderer';
|
||||
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
// Modal creates a portal. Overriding ReactDOM.createPortal allows portals to be tested in jest.
|
||||
ReactDOM.createPortal = node => node;
|
||||
// Modal creates a portal. Overriding createPortal allows portals to be tested in jest.
|
||||
jest.mock('react-dom', () => ({
|
||||
...jest.requireActual('react-dom'),
|
||||
createPortal: jest.fn(node => node), // Mock portal behavior
|
||||
}));
|
||||
|
||||
import { ConfirmationModal } from './ConfirmationModal'; // eslint-disable-line import/first
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Button, Hyperlink } from '@edx/paragon';
|
||||
import { Button, Hyperlink } from '@openedx/paragon';
|
||||
|
||||
// Actions
|
||||
import {
|
||||
@@ -59,6 +59,7 @@ export class DeleteAccount extends React.Component {
|
||||
hasLinkedTPA, isVerifiedAccount, status, errorType, intl,
|
||||
} = this.props;
|
||||
const canDelete = isVerifiedAccount && !hasLinkedTPA;
|
||||
const supportArticleUrl = process.env.SUPPORT_URL_TO_UNLINK_SOCIAL_MEDIA_ACCOUNT;
|
||||
|
||||
// TODO: We lack a good way of providing custom language for a particular site. This is a hack
|
||||
// to allow edx.org to fulfill its business requirements.
|
||||
@@ -75,67 +76,74 @@ export class DeleteAccount extends React.Component {
|
||||
<h2 className="section-heading h4 mb-3">
|
||||
{intl.formatMessage(messages['account.settings.delete.account.header'])}
|
||||
</h2>
|
||||
<p>{intl.formatMessage(messages['account.settings.delete.account.subheader'])}</p>
|
||||
<p>
|
||||
{intl.formatMessage(
|
||||
messages['account.settings.delete.account.text.1'],
|
||||
{ siteName: getConfig().SITE_NAME },
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{intl.formatMessage(
|
||||
messages[deleteAccountText2MessageKey],
|
||||
{ siteName: getConfig().SITE_NAME },
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
<PrintingInstructions />
|
||||
</p>
|
||||
<p className="text-danger h6">
|
||||
{intl.formatMessage(
|
||||
messages['account.settings.delete.account.text.warning'],
|
||||
{ siteName: getConfig().SITE_NAME },
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
<Hyperlink destination="https://support.edx.org/hc/en-us/sections/115004139268-Manage-Your-Account-Settings">
|
||||
{intl.formatMessage(messages['account.settings.delete.account.text.change.instead'])}
|
||||
</Hyperlink>
|
||||
</p>
|
||||
<p>
|
||||
<Button
|
||||
variant="outline-danger"
|
||||
onClick={canDelete ? this.props.deleteAccountConfirmation : null}
|
||||
disabled={!canDelete}
|
||||
>
|
||||
{intl.formatMessage(messages['account.settings.delete.account.button'])}
|
||||
</Button>
|
||||
</p>
|
||||
{
|
||||
this.props.canDeleteAccount ? (
|
||||
<>
|
||||
<p>{intl.formatMessage(messages['account.settings.delete.account.subheader'])}</p>
|
||||
<p>
|
||||
{intl.formatMessage(
|
||||
messages['account.settings.delete.account.text.1'],
|
||||
{ siteName: getConfig().SITE_NAME },
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{intl.formatMessage(
|
||||
messages[deleteAccountText2MessageKey],
|
||||
{ siteName: getConfig().SITE_NAME },
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
<PrintingInstructions />
|
||||
</p>
|
||||
<p className="text-danger h6">
|
||||
{intl.formatMessage(
|
||||
messages['account.settings.delete.account.text.warning'],
|
||||
{ siteName: getConfig().SITE_NAME },
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
<Hyperlink destination="https://help.edx.org/edxlearner/s/topic/0TOQq0000001UdZOAU/account-basics">
|
||||
{intl.formatMessage(messages['account.settings.delete.account.text.change.instead'])}
|
||||
</Hyperlink>
|
||||
</p>
|
||||
<p>
|
||||
<Button
|
||||
variant="outline-danger"
|
||||
onClick={canDelete ? this.props.deleteAccountConfirmation : null}
|
||||
disabled={!canDelete}
|
||||
>
|
||||
{intl.formatMessage(messages['account.settings.delete.account.button'])}
|
||||
</Button>
|
||||
</p>
|
||||
{isVerifiedAccount ? null : (
|
||||
<BeforeProceedingBanner
|
||||
instructionMessageId={optInInstructionMessageId}
|
||||
supportArticleUrl="https://support.edx.org/hc/en-us/articles/115000940568-How-do-I-confirm-my-email"
|
||||
/>
|
||||
)}
|
||||
{hasLinkedTPA ? (
|
||||
<BeforeProceedingBanner
|
||||
instructionMessageId="account.settings.delete.account.please.unlink"
|
||||
supportArticleUrl={supportArticleUrl}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{isVerifiedAccount ? null : (
|
||||
<BeforeProceedingBanner
|
||||
instructionMessageId={optInInstructionMessageId}
|
||||
supportArticleUrl="https://support.edx.org/hc/en-us/articles/115000940568-How-do-I-confirm-my-email-"
|
||||
/>
|
||||
)}
|
||||
<ConnectedConfirmationModal
|
||||
status={status}
|
||||
errorType={errorType}
|
||||
onSubmit={this.handleSubmit}
|
||||
onCancel={this.handleCancel}
|
||||
onChange={this.handlePasswordChange}
|
||||
password={this.state.password}
|
||||
/>
|
||||
|
||||
{hasLinkedTPA ? (
|
||||
<BeforeProceedingBanner
|
||||
instructionMessageId="account.settings.delete.account.please.unlink"
|
||||
supportArticleUrl="https://support.edx.org/hc/en-us/articles/207206067"
|
||||
/>
|
||||
) : null}
|
||||
<ConnectedSuccessModal status={status} onClose={this.handleFinalClose} />
|
||||
</>
|
||||
) : (
|
||||
<p>{intl.formatMessage(messages['account.settings.cannot.delete.account.text'])}</p>
|
||||
)
|
||||
}
|
||||
|
||||
<ConnectedConfirmationModal
|
||||
status={status}
|
||||
errorType={errorType}
|
||||
onSubmit={this.handleSubmit}
|
||||
onCancel={this.handleCancel}
|
||||
onChange={this.handlePasswordChange}
|
||||
password={this.state.password}
|
||||
/>
|
||||
|
||||
<ConnectedSuccessModal status={status} onClose={this.handleFinalClose} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -151,6 +159,7 @@ DeleteAccount.propTypes = {
|
||||
errorType: PropTypes.oneOf(['empty-password', 'server']),
|
||||
hasLinkedTPA: PropTypes.bool,
|
||||
isVerifiedAccount: PropTypes.bool,
|
||||
canDeleteAccount: PropTypes.bool,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
@@ -159,6 +168,7 @@ DeleteAccount.defaultProps = {
|
||||
isVerifiedAccount: true,
|
||||
status: null,
|
||||
errorType: null,
|
||||
canDeleteAccount: true,
|
||||
};
|
||||
|
||||
// Assume we're part of the accountSettings state.
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import React from 'react';
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink } from '@edx/paragon';
|
||||
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink } from '@openedx/paragon';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import messages from './messages';
|
||||
|
||||
const PrintingInstructions = (props) => {
|
||||
const PrintingInstructions = () => {
|
||||
const intl = useIntl();
|
||||
const actionLink = (
|
||||
<Hyperlink
|
||||
// TODO: What would a generic version of this link look like? Should
|
||||
// CERTIFICATE_SHARING_HELP_URL really be a configuration variable? In the meantime,
|
||||
// We've removed the link from the default message.
|
||||
destination="https://support.edx.org/hc/en-us/sections/115004173027-Receive-and-Share-edX-Certificates"
|
||||
destination="https://help.edx.org/edxlearner/s/topic/0TOQq0000001UVVOA2/certificates"
|
||||
>
|
||||
{props.intl.formatMessage(messages['account.settings.delete.account.text.3.link'])}
|
||||
{intl.formatMessage(messages['account.settings.delete.account.text.3.link'])}
|
||||
</Hyperlink>
|
||||
);
|
||||
|
||||
@@ -40,8 +40,4 @@ const PrintingInstructions = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
PrintingInstructions.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(PrintingInstructions);
|
||||
export default PrintingInstructions;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { ModalLayer, ModalCloseButton } from '@edx/paragon';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { ModalLayer, ModalCloseButton } from '@openedx/paragon';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
export const SuccessModal = (props) => {
|
||||
const { status, intl, onClose } = props;
|
||||
const intl = useIntl();
|
||||
const { status, onClose } = props;
|
||||
return (
|
||||
|
||||
<ModalLayer isOpen={status === 'deleted'} onClose={onClose}>
|
||||
@@ -20,7 +20,7 @@ export const SuccessModal = (props) => {
|
||||
</p>
|
||||
</div>
|
||||
<p>
|
||||
<ModalCloseButton className="float-right" variant="link">Close</ModalCloseButton>
|
||||
<ModalCloseButton className="float-right" variant="link">{intl.formatMessage(messages['account.settings.delete.account.modal.after.button'])}</ModalCloseButton>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -31,7 +31,6 @@ export const SuccessModal = (props) => {
|
||||
|
||||
SuccessModal.propTypes = {
|
||||
status: PropTypes.oneOf(['confirming', 'pending', 'deleted', 'failed']),
|
||||
intl: intlShape.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
@@ -39,4 +38,4 @@ SuccessModal.defaultProps = {
|
||||
status: null,
|
||||
};
|
||||
|
||||
export default injectIntl(SuccessModal);
|
||||
export default SuccessModal;
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import renderer from 'react-test-renderer';
|
||||
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
import { SuccessModal } from './SuccessModal';
|
||||
|
||||
// Modal creates a portal. Overriding ReactDOM.createPortal allows portals to be tested in jest.
|
||||
ReactDOM.createPortal = node => node;
|
||||
|
||||
import { SuccessModal } from './SuccessModal'; // eslint-disable-line import/first
|
||||
|
||||
const IntlSuccessModal = injectIntl(SuccessModal);
|
||||
// Modal creates a portal. Overriding createPortal allows portals to be tested in jest.
|
||||
jest.mock('react-dom', () => ({
|
||||
...jest.requireActual('react-dom'),
|
||||
createPortal: jest.fn(node => node), // Mock portal behavior
|
||||
}));
|
||||
|
||||
describe('SuccessModal', () => {
|
||||
let props = {};
|
||||
@@ -20,39 +19,40 @@ describe('SuccessModal', () => {
|
||||
};
|
||||
});
|
||||
|
||||
it('should match default closed success modal snapshot', () => {
|
||||
let tree = renderer.create((
|
||||
<IntlProvider locale="en"><IntlSuccessModal {...props} /></IntlProvider>))
|
||||
.toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
|
||||
tree = renderer.create((
|
||||
<IntlProvider locale="en"><IntlSuccessModal {...props} status="confirming" /></IntlProvider>))
|
||||
.toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
|
||||
tree = renderer.create((
|
||||
<IntlProvider locale="en"><IntlSuccessModal {...props} status="pending" /></IntlProvider>))
|
||||
.toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
|
||||
tree = renderer.create((
|
||||
<IntlProvider locale="en"><IntlSuccessModal {...props} status="failed" /></IntlProvider>))
|
||||
.toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
it('should match default closed success modal snapshot', async () => {
|
||||
await waitFor(() => {
|
||||
const tree = renderer.create((
|
||||
<IntlProvider locale="en"><SuccessModal {...props} /></IntlProvider>)).toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
await waitFor(() => {
|
||||
const tree = renderer.create((
|
||||
<IntlProvider locale="en"><SuccessModal {...props} status="confirming" /></IntlProvider>)).toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
await waitFor(() => {
|
||||
const tree = renderer.create((
|
||||
<IntlProvider locale="en"><SuccessModal {...props} status="pending" /></IntlProvider>)).toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
await waitFor(() => {
|
||||
const tree = renderer.create((
|
||||
<IntlProvider locale="en"><SuccessModal {...props} status="failed" /></IntlProvider>)).toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
it('should match open success modal snapshot', () => {
|
||||
const tree = renderer
|
||||
.create((
|
||||
it('should match open success modal snapshot', async () => {
|
||||
await waitFor(() => {
|
||||
const tree = renderer.create(
|
||||
<IntlProvider locale="en">
|
||||
<IntlSuccessModal
|
||||
<SuccessModal
|
||||
{...props}
|
||||
status="deleted" // This will cause 'modal-backdrop' and 'show' to appear on the modal as CSS classes.
|
||||
status="deleted"
|
||||
/>
|
||||
</IntlProvider>
|
||||
))
|
||||
.toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
</IntlProvider>,
|
||||
).toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`BeforeProceedingBanner should match the snapshot if SUPPORT_URL_TO_UNLINK_SOCIAL_MEDIA_ACCOUNT does not have a support link 1`] = `
|
||||
<div
|
||||
className="alert d-flex align-items-start alert-warning mt-n2"
|
||||
>
|
||||
<div>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-triangle-exclamation mr-2"
|
||||
data-icon="triangle-exclamation"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={{}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M256 32c14.2 0 27.3 7.5 34.5 19.8l216 368c7.3 12.4 7.3 27.7 .2 40.1S486.3 480 472 480L40 480c-14.3 0-27.6-7.7-34.7-20.1s-7-27.8 .2-40.1l216-368C228.7 39.5 241.8 32 256 32zm0 128c-13.3 0-24 10.7-24 24l0 112c0 13.3 10.7 24 24 24s24-10.7 24-24l0-112c0-13.3-10.7-24-24-24zm32 224a32 32 0 1 0 -64 0 32 32 0 1 0 64 0z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
Before proceeding, please unlink all social media accounts.
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`BeforeProceedingBanner should match the snapshot when SUPPORT_URL_TO_UNLINK_SOCIAL_MEDIA_ACCOUNT has a support link 1`] = `
|
||||
<div
|
||||
className="alert d-flex align-items-start alert-warning mt-n2"
|
||||
>
|
||||
<div>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-triangle-exclamation mr-2"
|
||||
data-icon="triangle-exclamation"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={{}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M256 32c14.2 0 27.3 7.5 34.5 19.8l216 368c7.3 12.4 7.3 27.7 .2 40.1S486.3 480 472 480L40 480c-14.3 0-27.6-7.7-34.7-20.1s-7-27.8 .2-40.1l216-368C228.7 39.5 241.8 32 256 32zm0 128c-13.3 0-24 10.7-24 24l0 112c0 13.3 10.7 24 24 24s24-10.7 24-24l0-112c0-13.3-10.7-24-24-24zm32 224a32 32 0 1 0 -64 0 32 32 0 1 0 64 0z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
Before proceeding, please
|
||||
<a
|
||||
className="pgn__hyperlink default-link standalone-link"
|
||||
href="http://test-support.edx"
|
||||
target="_self"
|
||||
>
|
||||
unlink all social media accounts
|
||||
</a>
|
||||
.
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -3,11 +3,11 @@
|
||||
exports[`ConfirmationModal should match default closed confirmation modal snapshot 1`] = `null`;
|
||||
|
||||
exports[`ConfirmationModal should match empty password confirmation modal snapshot 1`] = `
|
||||
Array [
|
||||
[
|
||||
<div
|
||||
data-focus-guard={true}
|
||||
style={
|
||||
Object {
|
||||
{
|
||||
"height": "0px",
|
||||
"left": "1px",
|
||||
"overflow": "hidden",
|
||||
@@ -35,12 +35,13 @@ Array [
|
||||
>
|
||||
<div
|
||||
className="pgn__modal-backdrop"
|
||||
data-testid="modal-backdrop"
|
||||
onClick={[MockFunction]}
|
||||
onKeyDown={[MockFunction]}
|
||||
/>
|
||||
<div
|
||||
aria-label="Are you sure?"
|
||||
className="pgn__modal pgn__modal-md pgn__modal-default pgn__alert-modal"
|
||||
className="pgn__modal pgn__modal-md pgn__modal-default pgn__modal-visible-overflow pgn__alert-modal"
|
||||
role="dialog"
|
||||
>
|
||||
<div
|
||||
@@ -68,19 +69,19 @@ Array [
|
||||
<div>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-exclamation-circle fa-w-16 mr-2"
|
||||
data-icon="exclamation-circle"
|
||||
className="svg-inline--fa fa-circle-exclamation mr-2"
|
||||
data-icon="circle-exclamation"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
style={{}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M504 256c0 136.997-111.043 248-248 248S8 392.997 8 256C8 119.083 119.043 8 256 8s248 111.083 248 248zm-248 50c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z"
|
||||
d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zm0-384c13.3 0 24 10.7 24 24l0 112c0 13.3-10.7 24-24 24s-24-10.7-24-24l0-112c0-13.3 10.7-24 24-24zM224 352a32 32 0 1 1 64 0 32 32 0 1 1 -64 0z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
style={{}}
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
@@ -101,19 +102,19 @@ Array [
|
||||
<div>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-exclamation-triangle fa-w-18 mr-2"
|
||||
data-icon="exclamation-triangle"
|
||||
className="svg-inline--fa fa-triangle-exclamation mr-2"
|
||||
data-icon="triangle-exclamation"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 576 512"
|
||||
style={{}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M569.517 440.013C587.975 472.007 564.806 512 527.94 512H48.054c-36.937 0-59.999-40.055-41.577-71.987L246.423 23.985c18.467-32.009 64.72-31.951 83.154 0l239.94 416.028zM288 354c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z"
|
||||
d="M256 32c14.2 0 27.3 7.5 34.5 19.8l216 368c7.3 12.4 7.3 27.7 .2 40.1S486.3 480 472 480L40 480c-14.3 0-27.6-7.7-34.7-20.1s-7-27.8 .2-40.1l216-368C228.7 39.5 241.8 32 256 32zm0 128c-13.3 0-24 10.7-24 24l0 112c0 13.3 10.7 24 24 24s24-10.7 24-24l0-112c0-13.3-10.7-24-24-24zm32 224a32 32 0 1 0 -64 0 32 32 0 1 0 64 0z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
style={{}}
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
@@ -130,29 +131,57 @@ Array [
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="form-group"
|
||||
className="pgn__form-group"
|
||||
for="passwordFieldId"
|
||||
>
|
||||
<label
|
||||
className="d-block"
|
||||
htmlFor="passwordFieldId"
|
||||
className="pgn__form-label d-block"
|
||||
htmlFor="form-field3"
|
||||
>
|
||||
If you still wish to continue and delete your account, please enter your account password:
|
||||
</label>
|
||||
<input
|
||||
aria-describedby="passwordFieldId-invalid-feedback"
|
||||
className="form-control is-invalid"
|
||||
id="passwordFieldId"
|
||||
name="password"
|
||||
onChange={[MockFunction]}
|
||||
type="password"
|
||||
value="fluffy bunnies"
|
||||
/>
|
||||
<strong
|
||||
className="invalid-feedback"
|
||||
id="passwordFieldId-invalid-feedback"
|
||||
<div
|
||||
className="pgn__form-control-decorator-group"
|
||||
>
|
||||
A password is required
|
||||
</strong>
|
||||
<input
|
||||
aria-describedby="form-field3-5"
|
||||
className="has-value form-control is-invalid"
|
||||
id="form-field3"
|
||||
name="password"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
type="password"
|
||||
value="fluffy bunnies"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="pgn__form-control-description pgn__form-text pgn__form-text-invalid"
|
||||
feedback-for="passwordFieldId"
|
||||
id="form-field3-5"
|
||||
>
|
||||
<span
|
||||
className="pgn__icon"
|
||||
>
|
||||
<svg
|
||||
aria-hidden={true}
|
||||
fill="none"
|
||||
focusable={false}
|
||||
height={24}
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width={24}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<div>
|
||||
A password is required
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -188,7 +217,7 @@ Array [
|
||||
<div
|
||||
data-focus-guard={true}
|
||||
style={
|
||||
Object {
|
||||
{
|
||||
"height": "0px",
|
||||
"left": "1px",
|
||||
"overflow": "hidden",
|
||||
@@ -204,11 +233,11 @@ Array [
|
||||
`;
|
||||
|
||||
exports[`ConfirmationModal should match open confirmation modal snapshot 1`] = `
|
||||
Array [
|
||||
[
|
||||
<div
|
||||
data-focus-guard={true}
|
||||
style={
|
||||
Object {
|
||||
{
|
||||
"height": "0px",
|
||||
"left": "1px",
|
||||
"overflow": "hidden",
|
||||
@@ -234,12 +263,13 @@ Array [
|
||||
>
|
||||
<div
|
||||
className="pgn__modal-backdrop"
|
||||
data-testid="modal-backdrop"
|
||||
onClick={[MockFunction]}
|
||||
onKeyDown={[MockFunction]}
|
||||
/>
|
||||
<div
|
||||
aria-label="Are you sure?"
|
||||
className="pgn__modal pgn__modal-md pgn__modal-default pgn__alert-modal"
|
||||
className="pgn__modal pgn__modal-md pgn__modal-default pgn__modal-visible-overflow pgn__alert-modal"
|
||||
role="dialog"
|
||||
>
|
||||
<div
|
||||
@@ -267,19 +297,19 @@ Array [
|
||||
<div>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-exclamation-triangle fa-w-18 mr-2"
|
||||
data-icon="exclamation-triangle"
|
||||
className="svg-inline--fa fa-triangle-exclamation mr-2"
|
||||
data-icon="triangle-exclamation"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 576 512"
|
||||
style={{}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M569.517 440.013C587.975 472.007 564.806 512 527.94 512H48.054c-36.937 0-59.999-40.055-41.577-71.987L246.423 23.985c18.467-32.009 64.72-31.951 83.154 0l239.94 416.028zM288 354c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z"
|
||||
d="M256 32c14.2 0 27.3 7.5 34.5 19.8l216 368c7.3 12.4 7.3 27.7 .2 40.1S486.3 480 472 480L40 480c-14.3 0-27.6-7.7-34.7-20.1s-7-27.8 .2-40.1l216-368C228.7 39.5 241.8 32 256 32zm0 128c-13.3 0-24 10.7-24 24l0 112c0 13.3 10.7 24 24 24s24-10.7 24-24l0-112c0-13.3-10.7-24-24-24zm32 224a32 32 0 1 0 -64 0 32 32 0 1 0 64 0z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
style={{}}
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
@@ -296,29 +326,28 @@ Array [
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="form-group"
|
||||
className="pgn__form-group"
|
||||
for="passwordFieldId"
|
||||
>
|
||||
<label
|
||||
className="d-block"
|
||||
htmlFor="passwordFieldId"
|
||||
className="pgn__form-label d-block"
|
||||
htmlFor="form-field1"
|
||||
>
|
||||
If you still wish to continue and delete your account, please enter your account password:
|
||||
</label>
|
||||
<input
|
||||
aria-describedby=""
|
||||
className="form-control"
|
||||
id="passwordFieldId"
|
||||
name="password"
|
||||
onChange={[MockFunction]}
|
||||
type="password"
|
||||
value="fluffy bunnies"
|
||||
/>
|
||||
<strong
|
||||
className="invalid-feedback"
|
||||
id="passwordFieldId-invalid-feedback"
|
||||
<div
|
||||
className="pgn__form-control-decorator-group"
|
||||
>
|
||||
Unable to delete account
|
||||
</strong>
|
||||
<input
|
||||
className="has-value form-control"
|
||||
id="form-field1"
|
||||
name="password"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
type="password"
|
||||
value="fluffy bunnies"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -354,7 +383,7 @@ Array [
|
||||
<div
|
||||
data-focus-guard={true}
|
||||
style={
|
||||
Object {
|
||||
{
|
||||
"height": "0px",
|
||||
"left": "1px",
|
||||
"overflow": "hidden",
|
||||
|
||||
@@ -27,8 +27,7 @@ exports[`DeleteAccount should match default section snapshot 1`] = `
|
||||
<p>
|
||||
<a
|
||||
className="pgn__hyperlink default-link standalone-link"
|
||||
href="https://support.edx.org/hc/en-us/sections/115004139268-Manage-Your-Account-Settings"
|
||||
onClick={[Function]}
|
||||
href="https://help.edx.org/edxlearner/s/topic/0TOQq0000001UdZOAU/account-basics"
|
||||
target="_self"
|
||||
>
|
||||
Want to change your email, name, or password instead?
|
||||
@@ -74,8 +73,7 @@ exports[`DeleteAccount should match unverified account section snapshot 1`] = `
|
||||
<p>
|
||||
<a
|
||||
className="pgn__hyperlink default-link standalone-link"
|
||||
href="https://support.edx.org/hc/en-us/sections/115004139268-Manage-Your-Account-Settings"
|
||||
onClick={[Function]}
|
||||
href="https://help.edx.org/edxlearner/s/topic/0TOQq0000001UdZOAU/account-basics"
|
||||
target="_self"
|
||||
>
|
||||
Want to change your email, name, or password instead?
|
||||
@@ -97,19 +95,19 @@ exports[`DeleteAccount should match unverified account section snapshot 1`] = `
|
||||
<div>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-exclamation-triangle fa-w-18 mr-2"
|
||||
data-icon="exclamation-triangle"
|
||||
className="svg-inline--fa fa-triangle-exclamation mr-2"
|
||||
data-icon="triangle-exclamation"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 576 512"
|
||||
style={{}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M569.517 440.013C587.975 472.007 564.806 512 527.94 512H48.054c-36.937 0-59.999-40.055-41.577-71.987L246.423 23.985c18.467-32.009 64.72-31.951 83.154 0l239.94 416.028zM288 354c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z"
|
||||
d="M256 32c14.2 0 27.3 7.5 34.5 19.8l216 368c7.3 12.4 7.3 27.7 .2 40.1S486.3 480 472 480L40 480c-14.3 0-27.6-7.7-34.7-20.1s-7-27.8 .2-40.1l216-368C228.7 39.5 241.8 32 256 32zm0 128c-13.3 0-24 10.7-24 24l0 112c0 13.3 10.7 24 24 24s24-10.7 24-24l0-112c0-13.3-10.7-24-24-24zm32 224a32 32 0 1 0 -64 0 32 32 0 1 0 64 0z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
style={{}}
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
@@ -117,8 +115,7 @@ exports[`DeleteAccount should match unverified account section snapshot 1`] = `
|
||||
Before proceeding, please
|
||||
<a
|
||||
className="pgn__hyperlink default-link standalone-link"
|
||||
href="https://support.edx.org/hc/en-us/articles/115000940568-How-do-I-confirm-my-email-"
|
||||
onClick={[Function]}
|
||||
href="https://support.edx.org/hc/en-us/articles/115000940568-How-do-I-confirm-my-email"
|
||||
target="_self"
|
||||
>
|
||||
activate your account
|
||||
@@ -156,8 +153,7 @@ exports[`DeleteAccount should match unverified account section snapshot 2`] = `
|
||||
<p>
|
||||
<a
|
||||
className="pgn__hyperlink default-link standalone-link"
|
||||
href="https://support.edx.org/hc/en-us/sections/115004139268-Manage-Your-Account-Settings"
|
||||
onClick={[Function]}
|
||||
href="https://help.edx.org/edxlearner/s/topic/0TOQq0000001UdZOAU/account-basics"
|
||||
target="_self"
|
||||
>
|
||||
Want to change your email, name, or password instead?
|
||||
@@ -179,19 +175,19 @@ exports[`DeleteAccount should match unverified account section snapshot 2`] = `
|
||||
<div>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-exclamation-triangle fa-w-18 mr-2"
|
||||
data-icon="exclamation-triangle"
|
||||
className="svg-inline--fa fa-triangle-exclamation mr-2"
|
||||
data-icon="triangle-exclamation"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 576 512"
|
||||
style={{}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M569.517 440.013C587.975 472.007 564.806 512 527.94 512H48.054c-36.937 0-59.999-40.055-41.577-71.987L246.423 23.985c18.467-32.009 64.72-31.951 83.154 0l239.94 416.028zM288 354c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z"
|
||||
d="M256 32c14.2 0 27.3 7.5 34.5 19.8l216 368c7.3 12.4 7.3 27.7 .2 40.1S486.3 480 472 480L40 480c-14.3 0-27.6-7.7-34.7-20.1s-7-27.8 .2-40.1l216-368C228.7 39.5 241.8 32 256 32zm0 128c-13.3 0-24 10.7-24 24l0 112c0 13.3 10.7 24 24 24s24-10.7 24-24l0-112c0-13.3-10.7-24-24-24zm32 224a32 32 0 1 0 -64 0 32 32 0 1 0 64 0z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
style={{}}
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
@@ -199,8 +195,7 @@ exports[`DeleteAccount should match unverified account section snapshot 2`] = `
|
||||
Before proceeding, please
|
||||
<a
|
||||
className="pgn__hyperlink default-link standalone-link"
|
||||
href="https://support.edx.org/hc/en-us/articles/207206067"
|
||||
onClick={[Function]}
|
||||
href="https://help.edx.org/edxlearner/s/article/How-do-I-link-or-unlink-my-edX-account-to-a-social-media-account"
|
||||
target="_self"
|
||||
>
|
||||
unlink all social media accounts
|
||||
|
||||
@@ -9,11 +9,11 @@ exports[`SuccessModal should match default closed success modal snapshot 3`] = `
|
||||
exports[`SuccessModal should match default closed success modal snapshot 4`] = `null`;
|
||||
|
||||
exports[`SuccessModal should match open success modal snapshot 1`] = `
|
||||
Array [
|
||||
[
|
||||
<div
|
||||
data-focus-guard={true}
|
||||
style={
|
||||
Object {
|
||||
{
|
||||
"height": "0px",
|
||||
"left": "1px",
|
||||
"overflow": "hidden",
|
||||
@@ -39,6 +39,7 @@ Array [
|
||||
>
|
||||
<div
|
||||
className="pgn__modal-backdrop"
|
||||
data-testid="modal-backdrop"
|
||||
onClick={[MockFunction]}
|
||||
onKeyDown={[MockFunction]}
|
||||
/>
|
||||
@@ -73,7 +74,7 @@ Array [
|
||||
<div
|
||||
data-focus-guard={true}
|
||||
style={
|
||||
Object {
|
||||
{
|
||||
"height": "0px",
|
||||
"left": "1px",
|
||||
"overflow": "hidden",
|
||||
|
||||
65
src/account-settings/delete-account/data/service.test.js
Normal file
65
src/account-settings/delete-account/data/service.test.js
Normal file
@@ -0,0 +1,65 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import formurlencoded from 'form-urlencoded';
|
||||
import { handleRequestError } from '../../data/utils';
|
||||
|
||||
import { postDeleteAccount } from './service';
|
||||
|
||||
jest.mock('@edx/frontend-platform');
|
||||
jest.mock('@edx/frontend-platform/auth');
|
||||
jest.mock('form-urlencoded');
|
||||
jest.mock('../../data/utils');
|
||||
|
||||
describe('postDeleteAccount', () => {
|
||||
const mockPost = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
|
||||
getConfig.mockReturnValue({
|
||||
LMS_BASE_URL: 'http://testserver',
|
||||
});
|
||||
|
||||
getAuthenticatedHttpClient.mockReturnValue({
|
||||
post: mockPost,
|
||||
});
|
||||
|
||||
formurlencoded.mockImplementation(obj => `encoded:${JSON.stringify(obj)}`);
|
||||
});
|
||||
|
||||
it('posts delete account request with password', async () => {
|
||||
const mockResponse = { data: { success: true } };
|
||||
mockPost.mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const result = await postDeleteAccount('mypassword');
|
||||
|
||||
expect(getConfig).toHaveBeenCalled();
|
||||
expect(getAuthenticatedHttpClient).toHaveBeenCalled();
|
||||
expect(formurlencoded).toHaveBeenCalledWith({ password: 'mypassword' });
|
||||
|
||||
expect(mockPost).toHaveBeenCalledWith(
|
||||
'http://testserver/api/user/v1/accounts/deactivate_logout/',
|
||||
'encoded:{"password":"mypassword"}',
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).toEqual(mockResponse.data);
|
||||
});
|
||||
|
||||
it('calls handleRequestError and throws when request fails', async () => {
|
||||
const mockError = new Error('Request failed');
|
||||
mockPost.mockRejectedValueOnce(mockError);
|
||||
|
||||
handleRequestError.mockImplementation(() => {
|
||||
throw mockError;
|
||||
});
|
||||
|
||||
await expect(postDeleteAccount('wrongpassword')).rejects.toThrow('Request failed');
|
||||
|
||||
expect(handleRequestError).toHaveBeenCalledWith(mockError);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,11 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
'account.settings.cannot.delete.account.text': {
|
||||
id: 'account.settings.cannot.delete.account.text',
|
||||
defaultMessage: 'Please note that, for legal and regulatory compliance purposes, account deletion is currently unavailable.',
|
||||
description: 'This text is visible when user is not allowed to delete account',
|
||||
},
|
||||
'account.settings.delete.account.header': {
|
||||
id: 'account.settings.delete.account.header',
|
||||
defaultMessage: 'Delete My Account',
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Form } from '@edx/paragon';
|
||||
import { DECLINED } from '../data/constants';
|
||||
|
||||
const Checkboxes = (props) => {
|
||||
const {
|
||||
id,
|
||||
options,
|
||||
values,
|
||||
onChange,
|
||||
} = props;
|
||||
|
||||
const [selected, setSelected] = useState(values);
|
||||
useEffect(() => {
|
||||
onChange(id, selected);
|
||||
}, [id, onChange, selected]);
|
||||
|
||||
const handleToggle = (value, option) => {
|
||||
// If the user checked 'declined', uncheck all other options
|
||||
if (value && option === DECLINED) {
|
||||
setSelected([DECLINED]);
|
||||
return;
|
||||
}
|
||||
|
||||
// If option checked, make sure this option is in `selected` (and remove 'declined')
|
||||
if (value && !selected.includes(option)) {
|
||||
const newSelected = selected.filter(i => i !== DECLINED).concat(option);
|
||||
setSelected(newSelected);
|
||||
}
|
||||
|
||||
// If unchecked, make sure this option is NOT in `selected`
|
||||
if (!value) {
|
||||
setSelected(selected.filter(i => i !== option));
|
||||
}
|
||||
};
|
||||
|
||||
const renderCheckboxes = () => options.map((option, index) => {
|
||||
const isFirst = index === 0;
|
||||
const isChecked = selected.includes(option.value);
|
||||
return (
|
||||
<div key={option.value} className="checkboxOption">
|
||||
<Form.Checkbox
|
||||
type="checkbox"
|
||||
id={option.value}
|
||||
name={option.value}
|
||||
value={option.value}
|
||||
checked={isChecked}
|
||||
autoFocus={isFirst}
|
||||
onChange={(event) => handleToggle(event.target.checked, option.value)}
|
||||
>
|
||||
{option.label}
|
||||
</Form.Checkbox>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div role="group">
|
||||
{renderCheckboxes()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Checkboxes.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
options: PropTypes.arrayOf(PropTypes.shape({
|
||||
value: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
})),
|
||||
values: PropTypes.arrayOf(PropTypes.string),
|
||||
onChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
Checkboxes.defaultProps = {
|
||||
options: [],
|
||||
values: [],
|
||||
};
|
||||
|
||||
export default Checkboxes;
|
||||
@@ -1,357 +0,0 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import {
|
||||
FormattedMessage,
|
||||
injectIntl,
|
||||
intlShape,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { Hyperlink, Form } from '@edx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import get from 'lodash.get';
|
||||
import isEmpty from 'lodash.isempty';
|
||||
import memoize from 'memoize-one';
|
||||
import { demographicsSectionSelector } from '../data/selectors';
|
||||
import EditableSelectField from '../EditableSelectField';
|
||||
import Checkboxes from './Checkboxes';
|
||||
import Alert from '../Alert';
|
||||
import { saveMultipleSettings, updateDraft } from '../data/actions';
|
||||
import {
|
||||
OTHER,
|
||||
SELF_DESCRIBE,
|
||||
} from '../data/constants';
|
||||
import messages from './DemographicsSection.messages';
|
||||
|
||||
class DemographicsSection extends React.Component {
|
||||
// We check the `demographicsOptions` prop to see if it is empty before we attempt to extract and
|
||||
// format the available options for each question from the API response.
|
||||
getApiOptions = memoize((demographicsOptions) => (this.hasRetrievedDemographicsOptions() && {
|
||||
demographicsGenderOptions: this.addDefaultOption('account.settings.field.demographics.gender.options.empty')
|
||||
.concat(demographicsOptions.actions.POST.gender.choices.map(key => ({
|
||||
value: key.value,
|
||||
label: key.display_name,
|
||||
}))),
|
||||
/* Ethnicity options don't need the blank/default option */
|
||||
demographicsEthnicityOptions: demographicsOptions.actions.POST.user_ethnicity.child.children.ethnicity.choices.map(
|
||||
key => ({
|
||||
value: key.value,
|
||||
label: key.display_name,
|
||||
}),
|
||||
),
|
||||
demographicsIncomeOptions: this.addDefaultOption('account.settings.field.demographics.income.options.empty')
|
||||
.concat(demographicsOptions.actions.POST.income.choices.map(key => ({
|
||||
value: key.value,
|
||||
label: key.display_name,
|
||||
}))),
|
||||
demographicsMilitaryHistoryOptions: this.addDefaultOption('account.settings.field.demographics.military_history.options.empty')
|
||||
.concat(demographicsOptions.actions.POST.military_history.choices.map(key => ({
|
||||
value: key.value,
|
||||
label: key.display_name,
|
||||
}))),
|
||||
demographicsEducationLevelOptions: this.addDefaultOption('account.settings.field.demographics.education_level.options.empty')
|
||||
.concat(demographicsOptions.actions.POST.learner_education_level.choices.map(key => ({
|
||||
value: key.value,
|
||||
label: key.display_name,
|
||||
}))),
|
||||
demographicsWorkStatusOptions: this.addDefaultOption('account.settings.field.demographics.work_status.options.empty')
|
||||
.concat(demographicsOptions.actions.POST.work_status.choices.map(key => ({
|
||||
value: key.value,
|
||||
label: key.display_name,
|
||||
}))),
|
||||
demographicsWorkSectorOptions: this.addDefaultOption('account.settings.field.demographics.work_sector.options.empty')
|
||||
.concat(demographicsOptions.actions.POST.current_work_sector.choices.map(key => ({
|
||||
value: key.value,
|
||||
label: key.display_name,
|
||||
}))),
|
||||
}));
|
||||
|
||||
ethnicityFieldDisplay = (demographicsEthnicityOptions) => {
|
||||
let ethnicities = [];
|
||||
if (get(this, 'props.formValues.demographics_user_ethnicity')) {
|
||||
ethnicities = this.props.formValues.demographics_user_ethnicity;
|
||||
}
|
||||
return ethnicities.map((e) => {
|
||||
const matchingOption = demographicsEthnicityOptions.filter(option => option.value === e)[0];
|
||||
return matchingOption && matchingOption.label;
|
||||
}).join(', ');
|
||||
};
|
||||
|
||||
handleEditableFieldChange = (name, value) => {
|
||||
this.props.updateDraft(name, value);
|
||||
};
|
||||
|
||||
handleSubmit = (formId) => {
|
||||
// We have some custom fields in this section. Instead of relying on the
|
||||
// submitted values, submit the values stored in 'drafts'.
|
||||
const { drafts } = this.props;
|
||||
const settingsArray = Object.entries(drafts).map(([field, value]) => ({
|
||||
formId: field,
|
||||
commitValues: value,
|
||||
}));
|
||||
|
||||
this.props.saveMultipleSettings(settingsArray, formId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Utility method that adds the specified message as a default option to the list of available
|
||||
* choices.
|
||||
*
|
||||
* @param {*} messageId id of message matching desired default label text
|
||||
*/
|
||||
addDefaultOption(messageId) {
|
||||
return [{
|
||||
value: '',
|
||||
label: this.props.intl.formatMessage(messages[messageId]),
|
||||
}];
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method that helps determine if we were able to retrieve the available options for
|
||||
* the Demographics questions. Returns true if the `demographicsOptions` prop is _not_ empty,
|
||||
* otherwise false. This prop being empty is indicative of a failure communicating with the
|
||||
* Demographics IDA's API.
|
||||
*/
|
||||
hasRetrievedDemographicsOptions() {
|
||||
return !isEmpty(this.props.formValues.demographicsOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* If an error is encountered when trying to communicate with the Demographics IDA then we will
|
||||
* display an Alert letting the user know that their info will not be displayed and temporarily
|
||||
* cannot be updated.
|
||||
*/
|
||||
renderDemographicsServiceIssueWarning() {
|
||||
if (!isEmpty(this.props.formErrors.demographicsError)
|
||||
|| !this.hasRetrievedDemographicsOptions()) {
|
||||
return (
|
||||
<div
|
||||
tabIndex="-1"
|
||||
ref={this.alertRef}
|
||||
>
|
||||
<Alert className="alert alert-danger" role="alert">
|
||||
<FormattedMessage
|
||||
id="account.settings.message.demographics.service.issue"
|
||||
defaultMessage="An error occurred attempting to retrieve or save your account information. Please try again later."
|
||||
description="alert message informing the user that the there is a problem retrieving or updating information from the Demographics microservice"
|
||||
/>
|
||||
</Alert>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
render() {
|
||||
const editableFieldProps = {
|
||||
onChange: this.handleEditableFieldChange,
|
||||
onSubmit: this.handleSubmit,
|
||||
};
|
||||
|
||||
const {
|
||||
demographicsGenderOptions,
|
||||
demographicsEthnicityOptions,
|
||||
demographicsIncomeOptions,
|
||||
demographicsMilitaryHistoryOptions,
|
||||
demographicsEducationLevelOptions,
|
||||
demographicsWorkStatusOptions,
|
||||
demographicsWorkSectorOptions,
|
||||
} = this.getApiOptions(this.props.formValues.demographicsOptions);
|
||||
|
||||
const showSelfDescribe = this.props.formValues.demographics_gender === SELF_DESCRIBE;
|
||||
const showWorkStatusDescribe = this.props.formValues.demographics_work_status === OTHER;
|
||||
|
||||
return (
|
||||
<div className="account-section pt-3 mb-5" id="demographics-information" ref={this.props.forwardRef}>
|
||||
<h2 className="section-heading h4 mb-3">
|
||||
{this.props.intl.formatMessage(messages['account.settings.section.demographics.information'])}
|
||||
</h2>
|
||||
<p>
|
||||
<Hyperlink
|
||||
destination={`${getConfig().MARKETING_SITE_BASE_URL}/demographics`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{this.props.intl.formatMessage(
|
||||
messages['account.settings.section.demographics.why'],
|
||||
{
|
||||
siteName: getConfig().SITE_NAME,
|
||||
},
|
||||
)}
|
||||
</Hyperlink>
|
||||
</p>
|
||||
{this.renderDemographicsServiceIssueWarning()}
|
||||
{/*
|
||||
If the demographicsOptions props are empty then there is no need to display the fields as
|
||||
the user will not have any choices available to select, nor will they be able to update
|
||||
their answers.
|
||||
*/}
|
||||
{this.hasRetrievedDemographicsOptions() && (
|
||||
<div id="demographics-fields">
|
||||
<EditableSelectField
|
||||
name="demographics_gender"
|
||||
type="select"
|
||||
value={this.props.formValues.demographics_gender}
|
||||
userSuppliedValue={showSelfDescribe ? this.props.formValues.demographics_gender_description : null}
|
||||
options={demographicsGenderOptions}
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.demographics.gender'])}
|
||||
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.demographics.gender.empty'])}
|
||||
{...editableFieldProps}
|
||||
>
|
||||
{showSelfDescribe && (
|
||||
<Form.Control
|
||||
name="demographics_gender_description"
|
||||
id="field-demographics_gender_description"
|
||||
type="text"
|
||||
placeholder={this.props.intl.formatMessage(messages['account.settings.field.demographics.gender_description.empty'])}
|
||||
value={this.props.formValues.demographics_gender_description}
|
||||
onChange={(e) => this.handleEditableFieldChange('demographics_gender_description', e.target.value)}
|
||||
aria-label={this.props.intl.formatMessage(messages['account.settings.field.demographics.gender_description'])}
|
||||
className="mt-1"
|
||||
/>
|
||||
)}
|
||||
</EditableSelectField>
|
||||
<EditableSelectField
|
||||
name="demographics_user_ethnicity"
|
||||
type="select"
|
||||
hidden
|
||||
value={this.ethnicityFieldDisplay(demographicsEthnicityOptions)}
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.demographics.ethnicity'])}
|
||||
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.demographics.ethnicity.empty'])}
|
||||
{...editableFieldProps}
|
||||
>
|
||||
<Checkboxes
|
||||
id="demographics_user_ethnicity"
|
||||
options={demographicsEthnicityOptions}
|
||||
values={this.props.formValues.demographics_user_ethnicity}
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
</EditableSelectField>
|
||||
<EditableSelectField
|
||||
name="demographics_income"
|
||||
type="select"
|
||||
value={this.props.formValues.demographics_income}
|
||||
options={demographicsIncomeOptions}
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.demographics.income'])}
|
||||
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.demographics.income.empty'])}
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
<EditableSelectField
|
||||
name="demographics_military_history"
|
||||
type="select"
|
||||
value={this.props.formValues.demographics_military_history}
|
||||
options={demographicsMilitaryHistoryOptions}
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.demographics.military_history'])}
|
||||
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.demographics.military_history.empty'])}
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
<EditableSelectField
|
||||
name="demographics_learner_education_level"
|
||||
type="select"
|
||||
value={this.props.formValues.demographics_learner_education_level}
|
||||
options={demographicsEducationLevelOptions}
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.demographics.learner_education_level'])}
|
||||
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.demographics.learner_education_level.empty'])}
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
<EditableSelectField
|
||||
name="demographics_parent_education_level"
|
||||
type="select"
|
||||
value={this.props.formValues.demographics_parent_education_level}
|
||||
options={demographicsEducationLevelOptions}
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.demographics.parent_education_level'])}
|
||||
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.demographics.parent_education_level.empty'])}
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
<EditableSelectField
|
||||
name="demographics_work_status"
|
||||
type="select"
|
||||
value={this.props.formValues.demographics_work_status}
|
||||
userSuppliedValue={showWorkStatusDescribe
|
||||
? this.props.formValues.demographics_work_status_description
|
||||
: null}
|
||||
options={demographicsWorkStatusOptions}
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.demographics.work_status'])}
|
||||
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.demographics.work_status.empty'])}
|
||||
{...editableFieldProps}
|
||||
>
|
||||
{showWorkStatusDescribe && (
|
||||
<Form.Control
|
||||
name="demographics_work_status_description"
|
||||
id="field-demographics_work_status_description"
|
||||
type="text"
|
||||
placeholder={this.props.intl.formatMessage(messages['account.settings.field.demographics.work_status_description.empty'])}
|
||||
value={this.props.formValues.demographics_work_status_description}
|
||||
onChange={(e) => this.handleEditableFieldChange('demographics_work_status_description', e.target.value)}
|
||||
aria-label={this.props.intl.formatMessage(messages['account.settings.field.demographics.work_status_description'])}
|
||||
className="mt-1"
|
||||
/>
|
||||
)}
|
||||
</EditableSelectField>
|
||||
<EditableSelectField
|
||||
name="demographics_current_work_sector"
|
||||
type="select"
|
||||
value={this.props.formValues.demographics_current_work_sector}
|
||||
options={demographicsWorkSectorOptions}
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.demographics.current_work_sector'])}
|
||||
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.demographics.current_work_sector.empty'])}
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
<EditableSelectField
|
||||
name="demographics_future_work_sector"
|
||||
type="select"
|
||||
value={this.props.formValues.demographics_future_work_sector}
|
||||
options={demographicsWorkSectorOptions}
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.demographics.future_work_sector'])}
|
||||
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.demographics.future_work_sector.empty'])}
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DemographicsSection.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
formValues: PropTypes.shape({
|
||||
demographics_gender: PropTypes.string,
|
||||
demographics_user_ethnicity: PropTypes.shape([]),
|
||||
demographics_income: PropTypes.string,
|
||||
demographics_military_history: PropTypes.string,
|
||||
demographics_learner_education_level: PropTypes.string,
|
||||
demographics_parent_education_level: PropTypes.string,
|
||||
demographics_work_status: PropTypes.string,
|
||||
demographics_current_work_sector: PropTypes.string,
|
||||
demographics_future_work_sector: PropTypes.string,
|
||||
demographics_work_status_description: PropTypes.string,
|
||||
demographics_gender_description: PropTypes.string,
|
||||
demographicsOptions: PropTypes.shape({}),
|
||||
}).isRequired,
|
||||
drafts: PropTypes.shape({
|
||||
demographics_gender: PropTypes.string,
|
||||
demographics_user_ethnicity: PropTypes.shape([]),
|
||||
demographics_income: PropTypes.string,
|
||||
demographics_military_history: PropTypes.string,
|
||||
demographics_learner_education_level: PropTypes.string,
|
||||
demographics_parent_education_level: PropTypes.string,
|
||||
demographics_work_status: PropTypes.string,
|
||||
demographics_current_work_sector: PropTypes.string,
|
||||
demographics_future_work_sector: PropTypes.string,
|
||||
demographics_work_status_description: PropTypes.string,
|
||||
demographics_gender_description: PropTypes.string,
|
||||
demographicsOptions: PropTypes.shape({}),
|
||||
}).isRequired,
|
||||
formErrors: PropTypes.shape({
|
||||
demographicsError: PropTypes.string,
|
||||
}).isRequired,
|
||||
forwardRef: PropTypes.func.isRequired,
|
||||
updateDraft: PropTypes.func.isRequired,
|
||||
saveMultipleSettings: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default connect(demographicsSectionSelector, {
|
||||
saveMultipleSettings,
|
||||
updateDraft,
|
||||
})(injectIntl(DemographicsSection));
|
||||
@@ -1,170 +0,0 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
/* Demographics section heading */
|
||||
'account.settings.section.demographics.information': {
|
||||
id: 'account.settings.section.demographics.information',
|
||||
defaultMessage: 'Optional Information',
|
||||
description: 'The optional information section heading.',
|
||||
},
|
||||
/* Gender identity */
|
||||
'account.settings.field.demographics.gender': {
|
||||
id: 'account.settings.field.demographics.gender',
|
||||
defaultMessage: 'Gender identity',
|
||||
description: 'Label for account settings gender identity field.',
|
||||
},
|
||||
'account.settings.field.demographics.gender.empty': {
|
||||
id: 'account.settings.field.demographics.gender.empty',
|
||||
defaultMessage: 'Add gender identity',
|
||||
description: 'Placeholder for empty account settings gender identity field.',
|
||||
},
|
||||
'account.settings.field.demographics.gender.options.empty': {
|
||||
id: 'account.settings.field.demographics.gender.options.empty',
|
||||
defaultMessage: 'Select a gender identity',
|
||||
description: 'Placeholder for the gender identity options dropdown.',
|
||||
},
|
||||
'account.settings.field.demographics.gender_description': {
|
||||
id: 'account.settings.field.demographics.gender_description',
|
||||
defaultMessage: 'Gender identity description',
|
||||
description: 'Label for account settings gender identity description field.',
|
||||
},
|
||||
'account.settings.field.demographics.gender_description.empty': {
|
||||
id: 'account.settings.field.demographics.gender_description.empty',
|
||||
defaultMessage: 'Enter description',
|
||||
description: 'Placeholder for empty account settings gender identity field.',
|
||||
},
|
||||
/* Ethnicity */
|
||||
'account.settings.field.demographics.ethnicity': {
|
||||
id: 'account.settings.field.demographics.ethnicity',
|
||||
defaultMessage: 'Race/Ethnicity identity',
|
||||
description: 'Label for account settings ethnic background field.',
|
||||
},
|
||||
'account.settings.field.demographics.ethnicity.empty': {
|
||||
id: 'account.settings.field.demographics.ethnicity.empty',
|
||||
defaultMessage: 'Add race/ethnicity identity',
|
||||
description: 'Placeholder for empty account settings ethnic background field.',
|
||||
},
|
||||
'account.settings.field.demographics.ethnicity.options.empty': {
|
||||
id: 'account.settings.field.demographics.ethnicity.options.empty',
|
||||
defaultMessage: 'Select all that apply', // TODO: Is this the desired text?
|
||||
description: 'Placeholder for the ethnic background options field.',
|
||||
},
|
||||
/* Income */
|
||||
'account.settings.field.demographics.income': {
|
||||
id: 'account.settings.field.demographics.income',
|
||||
defaultMessage: 'Family income',
|
||||
description: 'Label for account settings household income field.',
|
||||
},
|
||||
'account.settings.field.demographics.income.empty': {
|
||||
id: 'account.settings.field.demographics.income.empty',
|
||||
defaultMessage: 'Add family income',
|
||||
description: 'Placeholder for empty account settings household income field.',
|
||||
},
|
||||
'account.settings.field.demographics.income.options.empty': {
|
||||
id: 'account.settings.field.demographics.income.options.empty',
|
||||
defaultMessage: 'Select a family income range',
|
||||
description: 'Placeholder for the household income dropdown.',
|
||||
},
|
||||
/* Military history */
|
||||
'account.settings.field.demographics.military_history': {
|
||||
id: 'account.settings.field.demographics.military_history',
|
||||
defaultMessage: 'U.S. Military status',
|
||||
description: 'Label for account settings military history field.',
|
||||
},
|
||||
'account.settings.field.demographics.military_history.empty': {
|
||||
id: 'account.settings.field.demographics.military_history.empty',
|
||||
defaultMessage: 'Add military status',
|
||||
description: 'Placeholder for empty account settings military history field.',
|
||||
},
|
||||
'account.settings.field.demographics.military_history.options.empty': {
|
||||
id: 'account.settings.field.demographics.military_history.options.empty',
|
||||
defaultMessage: 'Select military status',
|
||||
description: 'Placeholder for the military history dropdown.',
|
||||
},
|
||||
/* Learner and family education level */
|
||||
'account.settings.field.demographics.learner_education_level': {
|
||||
id: 'account.settings.field.demographics.learner_education_level',
|
||||
defaultMessage: 'Your education level',
|
||||
description: 'Label for account settings learner education level field.',
|
||||
},
|
||||
'account.settings.field.demographics.learner_education_level.empty': {
|
||||
id: 'account.settings.field.demographics.learner_education_level.empty',
|
||||
defaultMessage: 'Add education level',
|
||||
description: 'Placeholder for empty account settings learner education level field.',
|
||||
},
|
||||
'account.settings.field.demographics.parent_education_level': {
|
||||
id: 'account.settings.field.demographics.parent_education_level',
|
||||
defaultMessage: 'Parents/Guardians education level',
|
||||
description: 'Label for account settings parent education level field.',
|
||||
},
|
||||
'account.settings.field.demographics.parent_education_level.empty': {
|
||||
id: 'account.settings.field.demographics.parent_education_level.empty',
|
||||
defaultMessage: 'Add education level',
|
||||
description: 'Placeholder for empty account settings parent education level field.',
|
||||
},
|
||||
'account.settings.field.demographics.education_level.options.empty': {
|
||||
id: 'account.settings.field.demographics.education_level.options.empty',
|
||||
defaultMessage: 'Select education level',
|
||||
description: 'Placeholder for the education level options dropdown.',
|
||||
},
|
||||
/* Work status */
|
||||
'account.settings.field.demographics.work_status': {
|
||||
id: 'account.settings.field.demographics.work_status',
|
||||
defaultMessage: 'Employment status',
|
||||
description: 'Label for account settings work status field.',
|
||||
},
|
||||
'account.settings.field.demographics.work_status.empty': {
|
||||
id: 'account.settings.field.demographics.work_status.empty',
|
||||
defaultMessage: 'Add employment status',
|
||||
description: 'Placeholder for empty account settings work status field.',
|
||||
},
|
||||
'account.settings.field.demographics.work_status.options.empty': {
|
||||
id: 'account.settings.field.demographics.work_status.options.empty',
|
||||
defaultMessage: 'Select employment status',
|
||||
description: 'Placeholder for the work status options dropdown.',
|
||||
},
|
||||
'account.settings.field.demographics.work_status_description': {
|
||||
id: 'account.settings.field.demographics.work_status_description',
|
||||
defaultMessage: 'Employment status description',
|
||||
description: 'Label for account settings work status description field.',
|
||||
},
|
||||
'account.settings.field.demographics.work_status_description.empty': {
|
||||
id: 'account.settings.field.demographics.work_status_description.empty',
|
||||
defaultMessage: 'Enter description',
|
||||
description: 'Placeholder for empty account settings work status description field.',
|
||||
},
|
||||
/* Work sector */
|
||||
'account.settings.field.demographics.current_work_sector': {
|
||||
id: 'account.settings.field.demographics.current_work_sector',
|
||||
defaultMessage: 'Current work industry',
|
||||
description: 'Label for account settings current work sector field.',
|
||||
},
|
||||
'account.settings.field.demographics.current_work_sector.empty': {
|
||||
id: 'account.settings.field.demographics.current_work_sector.empty',
|
||||
defaultMessage: 'Add work industry',
|
||||
description: 'Placeholder for empty account settings current work sector field.',
|
||||
},
|
||||
'account.settings.field.demographics.future_work_sector': {
|
||||
id: 'account.settings.field.demographics.future_work_sector',
|
||||
defaultMessage: 'Future work industry',
|
||||
description: 'Label for account settings future work sector field.',
|
||||
},
|
||||
'account.settings.field.demographics.future_work_sector.empty': {
|
||||
id: 'account.settings.field.demographics.future_work_sector.empty',
|
||||
defaultMessage: 'Add work industry',
|
||||
description: 'Placeholder for empty account settings future work sector field.',
|
||||
},
|
||||
'account.settings.field.demographics.work_sector.options.empty': {
|
||||
id: 'account.settings.field.demographics.work_sector.options.empty',
|
||||
defaultMessage: 'Select work industry',
|
||||
description: 'Placeholder for the work sector options dropdown.',
|
||||
},
|
||||
/* Legal copy link text */
|
||||
'account.settings.section.demographics.why': {
|
||||
id: 'account.settings.section.demographics.why',
|
||||
defaultMessage: 'Why does {siteName} collect this information?',
|
||||
description: 'Link text for a link to external legal text',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -1,140 +0,0 @@
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import get from 'lodash.get';
|
||||
import { convertData, TO, FROM } from './utils';
|
||||
|
||||
/**
|
||||
* Utility method that attempts to extract errors from the response of a PATCH request in order to
|
||||
* display a warning or otherwise meaningful message to the user.
|
||||
*
|
||||
* @param {Error} error
|
||||
*/
|
||||
export function createDemographicsError(error) {
|
||||
const apiError = Object.create(error);
|
||||
// If the error received has the `httpResponseData` field in it, then we should have reason to
|
||||
// believe the Demographics service is alive and responding. Extract errors from fields where
|
||||
// appropriate so we can display them to the user.
|
||||
if (get(error, 'customAttributes.httpErrorResponseData')) {
|
||||
apiError.fieldErrors = JSON.parse(error.customAttributes.httpErrorResponseData);
|
||||
if (get(apiError, 'fieldErrors.gender_description')) {
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
apiError.fieldErrors.demographics_gender = apiError.fieldErrors.gender_description[0];
|
||||
delete apiError.fieldErrors.gender_description;
|
||||
} else if (get(apiError, 'fieldErrors.work_status_description')) {
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
apiError.fieldErrors.demographics_work_status = apiError.fieldErrors.work_status_description[0];
|
||||
delete apiError.fieldErrors.work_status_description;
|
||||
}
|
||||
// Otherwise, when the service is down, the error response will not contain a
|
||||
// `httpErrorResponseData` field. Add a generic 'demographicsError' field to the fieldErrors that
|
||||
// will trigger showing an Alert to the user to them them know the update was unsuccessful.
|
||||
} else {
|
||||
apiError.fieldErrors = {
|
||||
demographicsError: error.customAttributes.httpErrorType,
|
||||
};
|
||||
}
|
||||
|
||||
return apiError;
|
||||
}
|
||||
|
||||
/**
|
||||
* post all of the data related to demographics.
|
||||
* @param {Number} userId users are identified in the api by LMS id
|
||||
* @param {Object} commitValues { demographics }
|
||||
*/
|
||||
export async function postDemographics(userId) {
|
||||
const requestConfig = { headers: { 'Content-Type': 'application/json' } };
|
||||
const requestUrl = `${getConfig().DEMOGRAPHICS_BASE_URL}/demographics/api/v1/demographics/`;
|
||||
const commitValues = { user: userId };
|
||||
let data = {};
|
||||
|
||||
({ data } = await getAuthenticatedHttpClient()
|
||||
.post(requestUrl, commitValues, requestConfig)
|
||||
.catch((error) => {
|
||||
const apiError = createDemographicsError(error);
|
||||
throw apiError;
|
||||
}));
|
||||
|
||||
return convertData(data, FROM);
|
||||
}
|
||||
|
||||
/**
|
||||
* get all data related to the demographics.
|
||||
* @param {Number} userId users are identified in the api by LMS id
|
||||
*/
|
||||
export async function getDemographics(userId) {
|
||||
const requestUrl = `${getConfig().DEMOGRAPHICS_BASE_URL}/demographics/api/v1/demographics/${userId}/`;
|
||||
let data = {};
|
||||
|
||||
try {
|
||||
({ data } = await getAuthenticatedHttpClient()
|
||||
.get(requestUrl));
|
||||
|
||||
data = convertData(data, FROM);
|
||||
} catch (error) {
|
||||
const apiError = Object.create(error);
|
||||
// if the API called resulted in this user receiving a 404 then follow up with a POST call to
|
||||
// try and create the demographics entity on the backend
|
||||
if (apiError.customAttributes.httpErrorStatus) {
|
||||
if (apiError.customAttributes.httpErrorStatus === 404) {
|
||||
data = await postDemographics(userId);
|
||||
}
|
||||
} else {
|
||||
data = {
|
||||
user: userId,
|
||||
demographics_gender: '',
|
||||
demographics_gender_description: '',
|
||||
demographics_income: '',
|
||||
demographics_learner_education_level: '',
|
||||
demographics_parent_education_level: '',
|
||||
demographics_military_history: '',
|
||||
demographics_work_status: '',
|
||||
demographics_work_status_description: '',
|
||||
demographics_current_work_sector: '',
|
||||
demographics_future_work_sector: '',
|
||||
demographics_user_ethnicity: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* patch all of the data related to demographics.
|
||||
* @param {Number} userId users are identified in the api by LMS id
|
||||
* @param {Object} commitValues { demographics }
|
||||
*/
|
||||
export async function patchDemographics(userId, commitValues) {
|
||||
const requestUrl = `${getConfig().DEMOGRAPHICS_BASE_URL}/demographics/api/v1/demographics/${userId}/`;
|
||||
const convertedCommitValues = convertData(commitValues, TO);
|
||||
let data = {};
|
||||
|
||||
({ data } = await getAuthenticatedHttpClient()
|
||||
.patch(requestUrl, convertedCommitValues)
|
||||
.catch((error) => {
|
||||
const apiError = createDemographicsError(error);
|
||||
throw apiError;
|
||||
}));
|
||||
|
||||
return convertData(data, FROM);
|
||||
}
|
||||
|
||||
/**
|
||||
* retrieve the options for each field from the Demographics API
|
||||
*/
|
||||
export async function getDemographicsOptions() {
|
||||
const requestUrl = `${getConfig().DEMOGRAPHICS_BASE_URL}/demographics/api/v1/demographics/`;
|
||||
let data = {};
|
||||
|
||||
try {
|
||||
({ data } = await getAuthenticatedHttpClient().options(requestUrl));
|
||||
} catch (error) {
|
||||
// We are catching and suppressing errors here on purpose. If an error occurs during the
|
||||
// getDemographicsOptions call we will pass back an empty `data` object. Downstream we make
|
||||
// the assumption that if the demographicsOptions object is empty that there was an issue or
|
||||
// error communicating with the service/API.
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
export const TO = 'to';
|
||||
export const FROM = 'from';
|
||||
export const DEMOGRAPHICS_FIELDS = [
|
||||
'demographics_gender',
|
||||
'demographics_gender_description',
|
||||
'demographics_income',
|
||||
'demographics_learner_education_level',
|
||||
'demographics_parent_education_level',
|
||||
'demographics_military_history',
|
||||
'demographics_work_status',
|
||||
'demographics_work_status_description',
|
||||
'demographics_current_work_sector',
|
||||
'demographics_future_work_sector',
|
||||
'demographics_user_ethnicity',
|
||||
];
|
||||
|
||||
// Frontend wants (example):
|
||||
// demographics_user_ethnicity: ["asian", "white", "other"]
|
||||
//
|
||||
// Demographics wants (example):
|
||||
// user_ethnicity: [
|
||||
// { ethnicity: "asian" },
|
||||
// { ethnicity: "white" },
|
||||
// { ethnicity: "other" }
|
||||
// ]
|
||||
function convertEthnicity(ethnicityData, direction) {
|
||||
if (direction === FROM) {
|
||||
return ethnicityData.map(e => e.ethnicity);
|
||||
}
|
||||
|
||||
if (direction === TO) {
|
||||
return ethnicityData.map(e => ({ ethnicity: e }));
|
||||
}
|
||||
|
||||
return ethnicityData;
|
||||
}
|
||||
|
||||
// Handles conversion of data to/from Demographics IDA to/from format needed for
|
||||
// frontend
|
||||
// * handles ethnicity field
|
||||
// * adds/removes 'demographics' to/from key
|
||||
// * replace `null` with empty string or empty string with null
|
||||
export function convertData(dataObject, direction) {
|
||||
const converted = {};
|
||||
|
||||
Object.entries(dataObject).forEach(([key, value]) => {
|
||||
let newValue = value;
|
||||
|
||||
if (key.includes('ethnicity')) {
|
||||
newValue = convertEthnicity(value, direction);
|
||||
}
|
||||
|
||||
if (direction === TO) {
|
||||
converted[key.replace('demographics_', '')] = newValue || null;
|
||||
}
|
||||
|
||||
if (direction === FROM) {
|
||||
converted[`demographics_${key}`] = newValue || '';
|
||||
}
|
||||
});
|
||||
|
||||
return converted;
|
||||
}
|
||||
@@ -1,584 +0,0 @@
|
||||
/* eslint-disable no-import-assign */
|
||||
import * as auth from '@edx/frontend-platform/auth';
|
||||
|
||||
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { Provider } from 'react-redux';
|
||||
import React from 'react';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import renderer from 'react-test-renderer';
|
||||
import DemographicsSection from '../DemographicsSection';
|
||||
|
||||
jest.mock('@edx/frontend-platform/auth');
|
||||
|
||||
const IntlDemographicsSection = injectIntl(DemographicsSection);
|
||||
|
||||
jest.mock('../../data/selectors', () => jest.fn().mockImplementation(() => ({ demographicsSectionSelector: () => ({}) })));
|
||||
|
||||
const mockStore = configureStore();
|
||||
|
||||
describe('DemographicsSection', () => {
|
||||
let props = {};
|
||||
let store = {};
|
||||
|
||||
const reduxWrapper = children => (
|
||||
<IntlProvider locale="en">
|
||||
<Provider store={store}>{children}</Provider>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
store = mockStore();
|
||||
props = {
|
||||
updateDraft: jest.fn(),
|
||||
formValues: {
|
||||
demographics_gender: 'declined',
|
||||
demographics_gender_description: '',
|
||||
demographics_user_ethnicity: [],
|
||||
demographics_income: 'declined',
|
||||
demographics_military_history: 'declined',
|
||||
demographics_learner_education_level: 'declined',
|
||||
demographics_parent_education_level: 'declined',
|
||||
demographics_work_status: 'declined',
|
||||
demographics_work_status_description: '',
|
||||
demographics_current_work_sector: 'declined',
|
||||
demographics_future_work_sector: 'declined',
|
||||
demographics_user: 1,
|
||||
demographicsOptions: {
|
||||
actions: {
|
||||
POST: {
|
||||
gender: {
|
||||
choices: [
|
||||
{
|
||||
value: 'woman',
|
||||
display_name: 'Woman',
|
||||
},
|
||||
{
|
||||
value: 'man',
|
||||
display_name: 'Man',
|
||||
},
|
||||
{
|
||||
value: 'non-binary',
|
||||
display_name: 'Non-binary',
|
||||
},
|
||||
{
|
||||
value: 'self-describe',
|
||||
display_name: 'Prefer to self describe',
|
||||
},
|
||||
{
|
||||
value: 'declined',
|
||||
display_name: 'Prefer not to respond',
|
||||
},
|
||||
],
|
||||
},
|
||||
income: {
|
||||
choices: [
|
||||
{
|
||||
value: 'less-than-10k',
|
||||
display_name: 'Less than US $10,000',
|
||||
},
|
||||
{
|
||||
value: '10k-25k',
|
||||
display_name: 'US $10,000 - $25,000',
|
||||
},
|
||||
{
|
||||
value: '25k-50k',
|
||||
display_name: 'US $25,000 - $50,000',
|
||||
},
|
||||
{
|
||||
value: '50k-75k',
|
||||
display_name: 'US $50,000 - $75,000',
|
||||
},
|
||||
{
|
||||
value: '75k-100k',
|
||||
display_name: 'US $75,000 - $100,000',
|
||||
},
|
||||
{
|
||||
value: 'over-100k',
|
||||
display_name: 'Over US $100,000',
|
||||
},
|
||||
{
|
||||
value: 'unsure',
|
||||
display_name: "I don't know",
|
||||
},
|
||||
{
|
||||
value: 'declined',
|
||||
display_name: 'Prefer not to respond',
|
||||
},
|
||||
],
|
||||
},
|
||||
learner_education_level: {
|
||||
choices: [
|
||||
{
|
||||
value: 'no-high-school',
|
||||
display_name: 'No High School',
|
||||
},
|
||||
{
|
||||
value: 'some-high-school',
|
||||
display_name: 'Some High School',
|
||||
},
|
||||
{
|
||||
value: 'high-school-ged-equivalent',
|
||||
display_name: 'High School diploma, GED, or equivalent',
|
||||
},
|
||||
{
|
||||
value: 'some-college',
|
||||
display_name: 'Some college, but no degree',
|
||||
},
|
||||
{
|
||||
value: 'associates',
|
||||
display_name: 'Associates degree',
|
||||
},
|
||||
{
|
||||
value: 'bachelors',
|
||||
display_name: 'Bachelors degree',
|
||||
},
|
||||
{
|
||||
value: 'masters',
|
||||
display_name: 'Masters degree',
|
||||
},
|
||||
{
|
||||
value: 'professional',
|
||||
display_name: 'Professional degree',
|
||||
},
|
||||
{
|
||||
value: 'doctorate',
|
||||
display_name: 'Doctorate degree',
|
||||
},
|
||||
{
|
||||
value: 'declined',
|
||||
display_name: 'Prefer not to respond',
|
||||
},
|
||||
],
|
||||
},
|
||||
parent_education_level: {
|
||||
choices: [
|
||||
{
|
||||
value: 'no-high-school',
|
||||
display_name: 'No High School',
|
||||
},
|
||||
{
|
||||
value: 'some-high-school',
|
||||
display_name: 'Some High School',
|
||||
},
|
||||
{
|
||||
value: 'high-school-ged-equivalent',
|
||||
display_name: 'High School diploma, GED, or equivalent',
|
||||
},
|
||||
{
|
||||
value: 'some-college',
|
||||
display_name: 'Some college, but no degree',
|
||||
},
|
||||
{
|
||||
value: 'associates',
|
||||
display_name: 'Associates degree',
|
||||
},
|
||||
{
|
||||
value: 'bachelors',
|
||||
display_name: 'Bachelors degree',
|
||||
},
|
||||
{
|
||||
value: 'masters',
|
||||
display_name: 'Masters degree',
|
||||
},
|
||||
{
|
||||
value: 'professional',
|
||||
display_name: 'Professional degree',
|
||||
},
|
||||
{
|
||||
value: 'doctorate',
|
||||
display_name: 'Doctorate degree',
|
||||
},
|
||||
{
|
||||
value: 'declined',
|
||||
display_name: 'Prefer not to respond',
|
||||
},
|
||||
],
|
||||
},
|
||||
military_history: {
|
||||
choices: [
|
||||
{
|
||||
value: 'never-served',
|
||||
display_name: 'Never served in the military',
|
||||
},
|
||||
{
|
||||
value: 'training',
|
||||
display_name: 'Only on active duty for training',
|
||||
},
|
||||
{
|
||||
value: 'active',
|
||||
display_name: 'Now on active duty',
|
||||
},
|
||||
{
|
||||
value: 'previously-active',
|
||||
display_name: 'On active duty in the past, but not now',
|
||||
},
|
||||
{
|
||||
value: 'declined',
|
||||
display_name: 'Prefer not to respond',
|
||||
},
|
||||
],
|
||||
},
|
||||
work_status: {
|
||||
choices: [
|
||||
{
|
||||
value: 'full-time',
|
||||
display_name: 'Employed, working full-time',
|
||||
},
|
||||
{
|
||||
value: 'part-time',
|
||||
display_name: 'Employed, working part-time',
|
||||
},
|
||||
{
|
||||
value: 'self-employed',
|
||||
display_name: 'Self-Employed',
|
||||
},
|
||||
{
|
||||
value: 'not-employed-looking',
|
||||
display_name: 'Not employed, looking for work',
|
||||
},
|
||||
{
|
||||
value: 'not-employed-not-looking',
|
||||
display_name: 'Not employed, not looking for work',
|
||||
},
|
||||
{
|
||||
value: 'unable',
|
||||
display_name: 'Unable to work',
|
||||
},
|
||||
{
|
||||
value: 'retired',
|
||||
display_name: 'Retired',
|
||||
},
|
||||
{
|
||||
value: 'other',
|
||||
display_name: 'Other',
|
||||
},
|
||||
{
|
||||
value: 'declined',
|
||||
display_name: 'Prefer not to respond',
|
||||
},
|
||||
],
|
||||
},
|
||||
current_work_sector: {
|
||||
choices: [
|
||||
{
|
||||
value: 'accommodation-food',
|
||||
display_name: 'Accommodation and Food Services',
|
||||
},
|
||||
{
|
||||
value: 'administrative-support-waste-remediation',
|
||||
display_name: 'Administrative and Support and Waste Management and Remediation Services',
|
||||
},
|
||||
{
|
||||
value: 'agriculture-forestry-fishing-hunting',
|
||||
display_name: 'Agriculture, Forestry, Fishing and Hunting',
|
||||
},
|
||||
{
|
||||
value: 'arts-entertainment-recreation',
|
||||
display_name: 'Arts, Entertainment, and Recreation',
|
||||
},
|
||||
{
|
||||
value: 'construction',
|
||||
display_name: 'Construction',
|
||||
},
|
||||
{
|
||||
value: 'educational',
|
||||
display_name: 'Education Services',
|
||||
},
|
||||
{
|
||||
value: 'finance-insurance',
|
||||
display_name: 'Finance and Insurance',
|
||||
},
|
||||
{
|
||||
value: 'healthcare-social',
|
||||
display_name: 'Health Care and Social Assistance',
|
||||
},
|
||||
{
|
||||
value: 'information',
|
||||
display_name: 'Information',
|
||||
},
|
||||
{
|
||||
value: 'management',
|
||||
display_name: 'Management of Companies and Enterprises',
|
||||
},
|
||||
{
|
||||
value: 'manufacturing',
|
||||
display_name: 'Manufacturing',
|
||||
},
|
||||
{
|
||||
value: 'mining-quarry-oil-gas',
|
||||
display_name: 'Mining, Quarrying, and Oil and Gas Extraction',
|
||||
},
|
||||
{
|
||||
value: 'professional-scientific-technical',
|
||||
display_name: 'Professional, Scientific, and Technical Services',
|
||||
},
|
||||
{
|
||||
value: 'public-admin',
|
||||
display_name: 'Public Administration',
|
||||
},
|
||||
{
|
||||
value: 'real-estate',
|
||||
display_name: 'Real Estate and Rental and Leasing',
|
||||
},
|
||||
{
|
||||
value: 'retail',
|
||||
display_name: 'Retail Trade',
|
||||
},
|
||||
{
|
||||
value: 'transport-warehousing',
|
||||
display_name: 'Transportation and Warehousing',
|
||||
},
|
||||
{
|
||||
value: 'utilities',
|
||||
display_name: 'Utilities',
|
||||
},
|
||||
{
|
||||
value: 'trade',
|
||||
display_name: 'Wholesale Trade',
|
||||
},
|
||||
{
|
||||
value: 'other',
|
||||
display_name: 'Other',
|
||||
},
|
||||
{
|
||||
value: 'declined',
|
||||
display_name: 'Prefer not to respond',
|
||||
},
|
||||
],
|
||||
},
|
||||
future_work_sector: {
|
||||
choices: [
|
||||
{
|
||||
value: 'accommodation-food',
|
||||
display_name: 'Accommodation and Food Services',
|
||||
},
|
||||
{
|
||||
value: 'administrative-support-waste-remediation',
|
||||
display_name: 'Administrative and Support and Waste Management and Remediation Services',
|
||||
},
|
||||
{
|
||||
value: 'agriculture-forestry-fishing-hunting',
|
||||
display_name: 'Agriculture, Forestry, Fishing and Hunting',
|
||||
},
|
||||
{
|
||||
value: 'arts-entertainment-recreation',
|
||||
display_name: 'Arts, Entertainment, and Recreation',
|
||||
},
|
||||
{
|
||||
value: 'construction',
|
||||
display_name: 'Construction',
|
||||
},
|
||||
{
|
||||
value: 'educational',
|
||||
display_name: 'Education Services',
|
||||
},
|
||||
{
|
||||
value: 'finance-insurance',
|
||||
display_name: 'Finance and Insurance',
|
||||
},
|
||||
{
|
||||
value: 'healthcare-social',
|
||||
display_name: 'Health Care and Social Assistance',
|
||||
},
|
||||
{
|
||||
value: 'information',
|
||||
display_name: 'Information',
|
||||
},
|
||||
{
|
||||
value: 'management',
|
||||
display_name: 'Management of Companies and Enterprises',
|
||||
},
|
||||
{
|
||||
value: 'manufacturing',
|
||||
display_name: 'Manufacturing',
|
||||
},
|
||||
{
|
||||
value: 'mining-quarry-oil-gas',
|
||||
display_name: 'Mining, Quarrying, and Oil and Gas Extraction',
|
||||
},
|
||||
{
|
||||
value: 'professional-scientific-technical',
|
||||
display_name: 'Professional, Scientific, and Technical Services',
|
||||
},
|
||||
{
|
||||
value: 'public-admin',
|
||||
display_name: 'Public Administration',
|
||||
},
|
||||
{
|
||||
value: 'real-estate',
|
||||
display_name: 'Real Estate and Rental and Leasing',
|
||||
},
|
||||
{
|
||||
value: 'retail',
|
||||
display_name: 'Retail Trade',
|
||||
},
|
||||
{
|
||||
value: 'transport-warehousing',
|
||||
display_name: 'Transportation and Warehousing',
|
||||
},
|
||||
{
|
||||
value: 'utilities',
|
||||
display_name: 'Utilities',
|
||||
},
|
||||
{
|
||||
value: 'trade',
|
||||
display_name: 'Wholesale Trade',
|
||||
},
|
||||
{
|
||||
value: 'other',
|
||||
display_name: 'Other',
|
||||
},
|
||||
{
|
||||
value: 'declined',
|
||||
display_name: 'Prefer not to respond',
|
||||
},
|
||||
],
|
||||
},
|
||||
user_ethnicity: {
|
||||
child: {
|
||||
children: {
|
||||
ethnicity: {
|
||||
choices: [
|
||||
{
|
||||
value: 'american-indian-or-alaska-native',
|
||||
display_name: 'American Indian or Alaska Native',
|
||||
},
|
||||
{
|
||||
value: 'asian',
|
||||
display_name: 'Asian',
|
||||
},
|
||||
{
|
||||
value: 'black-or-african-american',
|
||||
display_name: 'Black or African American',
|
||||
},
|
||||
{
|
||||
value: 'hispanic-latin-spanish',
|
||||
display_name: 'Hispanic, Latin, or Spanish origin',
|
||||
},
|
||||
{
|
||||
value: 'middle-eastern-or-north-african',
|
||||
display_name: 'Middle Eastern or North African',
|
||||
},
|
||||
{
|
||||
value: 'native-hawaiian-or-pacific-islander',
|
||||
display_name: 'Native Hawaiian or Other Pacific Islander',
|
||||
},
|
||||
{
|
||||
value: 'white',
|
||||
display_name: 'White',
|
||||
},
|
||||
{
|
||||
value: 'other',
|
||||
display_name: 'Some other race, ethnicity, or origin',
|
||||
},
|
||||
{
|
||||
value: 'declined',
|
||||
display_name: 'Prefer not to respond',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
formErrors: {},
|
||||
intl: {},
|
||||
forwardRef: () => {},
|
||||
drafts: {},
|
||||
};
|
||||
auth.getAuthenticatedHttpClient = jest.fn(() => ({
|
||||
patch: async () => ({
|
||||
data: { status: 200 },
|
||||
catch: () => {},
|
||||
}),
|
||||
}));
|
||||
auth.getAuthenticatedUser = jest.fn(() => ({ userId: 1 }));
|
||||
});
|
||||
|
||||
it('should render', () => {
|
||||
const wrapper = renderer.create(reduxWrapper(<IntlDemographicsSection {...props} />)).toJSON();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render an Alert if an error occurs', () => {
|
||||
props = {
|
||||
...props,
|
||||
formErrors: {
|
||||
demographicsError: 'api-error',
|
||||
},
|
||||
};
|
||||
|
||||
const wrapper = renderer.create(reduxWrapper(<IntlDemographicsSection {...props} />)).toJSON();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should set user input correctly when user provides gender self-description', () => {
|
||||
props = {
|
||||
...props,
|
||||
formValues: {
|
||||
...props.formValues,
|
||||
demographics_gender: 'self-describe',
|
||||
demographics_gender_description: 'test',
|
||||
},
|
||||
};
|
||||
|
||||
const wrapper = renderer.create(reduxWrapper(<IntlDemographicsSection {...props} />)).toJSON();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should set user input correctly when user provides answers to work_status question', () => {
|
||||
props = {
|
||||
...props,
|
||||
formValues: {
|
||||
...props.formValues,
|
||||
demographics_work_status: 'other',
|
||||
demographics_work_status_description: 'test',
|
||||
},
|
||||
};
|
||||
|
||||
const wrapper = renderer.create(reduxWrapper(<IntlDemographicsSection {...props} />)).toJSON();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render ethnicity text correctly', () => {
|
||||
props = {
|
||||
...props,
|
||||
formValues: {
|
||||
...props.formValues,
|
||||
demographics_user_ethnicity: ['asian'],
|
||||
},
|
||||
};
|
||||
|
||||
const wrapper = renderer.create(reduxWrapper(<IntlDemographicsSection {...props} />)).toJSON();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render ethnicity correctly when multiple options are selected', () => {
|
||||
props = {
|
||||
...props,
|
||||
formValues: {
|
||||
...props.formValues,
|
||||
demographics_user_ethnicity: ['hispanic-latin-spanish', 'white'],
|
||||
},
|
||||
};
|
||||
|
||||
const wrapper = renderer.create(reduxWrapper(<IntlDemographicsSection {...props} />)).toJSON();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render an Alert when demographicsOptions props are empty', () => {
|
||||
props = {
|
||||
...props,
|
||||
formValues: {
|
||||
demographicsOptions: null,
|
||||
},
|
||||
};
|
||||
|
||||
const wrapper = renderer.create(reduxWrapper(<IntlDemographicsSection {...props} />)).toJSON();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -1,3946 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`DemographicsSection should render 1`] = `
|
||||
<div
|
||||
className="account-section pt-3 mb-5"
|
||||
id="demographics-information"
|
||||
>
|
||||
<h2
|
||||
className="section-heading h4 mb-3"
|
||||
>
|
||||
Optional Information
|
||||
</h2>
|
||||
<p>
|
||||
<a
|
||||
className="pgn__hyperlink default-link standalone-link"
|
||||
href="http://localhost:5335/demographics"
|
||||
onClick={[Function]}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Why does localhost collect this information?
|
||||
<span
|
||||
className="pgn__hyperlink__external-icon"
|
||||
title="Opens in a new tab"
|
||||
>
|
||||
<span
|
||||
className="pgn__icon"
|
||||
style={
|
||||
Object {
|
||||
"height": "1em",
|
||||
"width": "1em",
|
||||
}
|
||||
}
|
||||
>
|
||||
<svg
|
||||
aria-hidden={true}
|
||||
fill="none"
|
||||
focusable={false}
|
||||
height={24}
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width={24}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M19 19H5V5h7V3H3v18h18v-9h-2v7ZM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
className="sr-only"
|
||||
>
|
||||
in a new tab
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</p>
|
||||
<div
|
||||
id="demographics-fields"
|
||||
>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Gender identity
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Race/Ethnicity identity
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
<button
|
||||
className="p-0 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Add race/ethnicity identity
|
||||
</button>
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Family income
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
U.S. Military status
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Your education level
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Parents/Guardians education level
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Employment status
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Current work industry
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Future work industry
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`DemographicsSection should render an Alert if an error occurs 1`] = `
|
||||
<div
|
||||
className="account-section pt-3 mb-5"
|
||||
id="demographics-information"
|
||||
>
|
||||
<h2
|
||||
className="section-heading h4 mb-3"
|
||||
>
|
||||
Optional Information
|
||||
</h2>
|
||||
<p>
|
||||
<a
|
||||
className="pgn__hyperlink default-link standalone-link"
|
||||
href="http://localhost:5335/demographics"
|
||||
onClick={[Function]}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Why does localhost collect this information?
|
||||
<span
|
||||
className="pgn__hyperlink__external-icon"
|
||||
title="Opens in a new tab"
|
||||
>
|
||||
<span
|
||||
className="pgn__icon"
|
||||
style={
|
||||
Object {
|
||||
"height": "1em",
|
||||
"width": "1em",
|
||||
}
|
||||
}
|
||||
>
|
||||
<svg
|
||||
aria-hidden={true}
|
||||
fill="none"
|
||||
focusable={false}
|
||||
height={24}
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width={24}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M19 19H5V5h7V3H3v18h18v-9h-2v7ZM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
className="sr-only"
|
||||
>
|
||||
in a new tab
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</p>
|
||||
<div
|
||||
tabIndex="-1"
|
||||
>
|
||||
<div
|
||||
className="alert d-flex align-items-start alert alert-danger"
|
||||
>
|
||||
<div />
|
||||
<div>
|
||||
An error occurred attempting to retrieve or save your account information. Please try again later.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
id="demographics-fields"
|
||||
>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Gender identity
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Race/Ethnicity identity
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
<button
|
||||
className="p-0 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Add race/ethnicity identity
|
||||
</button>
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Family income
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
U.S. Military status
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Your education level
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Parents/Guardians education level
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Employment status
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Current work industry
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Future work industry
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`DemographicsSection should render an Alert when demographicsOptions props are empty 1`] = `
|
||||
<div
|
||||
className="account-section pt-3 mb-5"
|
||||
id="demographics-information"
|
||||
>
|
||||
<h2
|
||||
className="section-heading h4 mb-3"
|
||||
>
|
||||
Optional Information
|
||||
</h2>
|
||||
<p>
|
||||
<a
|
||||
className="pgn__hyperlink default-link standalone-link"
|
||||
href="http://localhost:5335/demographics"
|
||||
onClick={[Function]}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Why does localhost collect this information?
|
||||
<span
|
||||
className="pgn__hyperlink__external-icon"
|
||||
title="Opens in a new tab"
|
||||
>
|
||||
<span
|
||||
className="pgn__icon"
|
||||
style={
|
||||
Object {
|
||||
"height": "1em",
|
||||
"width": "1em",
|
||||
}
|
||||
}
|
||||
>
|
||||
<svg
|
||||
aria-hidden={true}
|
||||
fill="none"
|
||||
focusable={false}
|
||||
height={24}
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width={24}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M19 19H5V5h7V3H3v18h18v-9h-2v7ZM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
className="sr-only"
|
||||
>
|
||||
in a new tab
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</p>
|
||||
<div
|
||||
tabIndex="-1"
|
||||
>
|
||||
<div
|
||||
className="alert d-flex align-items-start alert alert-danger"
|
||||
>
|
||||
<div />
|
||||
<div>
|
||||
An error occurred attempting to retrieve or save your account information. Please try again later.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`DemographicsSection should render ethnicity correctly when multiple options are selected 1`] = `
|
||||
<div
|
||||
className="account-section pt-3 mb-5"
|
||||
id="demographics-information"
|
||||
>
|
||||
<h2
|
||||
className="section-heading h4 mb-3"
|
||||
>
|
||||
Optional Information
|
||||
</h2>
|
||||
<p>
|
||||
<a
|
||||
className="pgn__hyperlink default-link standalone-link"
|
||||
href="http://localhost:5335/demographics"
|
||||
onClick={[Function]}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Why does localhost collect this information?
|
||||
<span
|
||||
className="pgn__hyperlink__external-icon"
|
||||
title="Opens in a new tab"
|
||||
>
|
||||
<span
|
||||
className="pgn__icon"
|
||||
style={
|
||||
Object {
|
||||
"height": "1em",
|
||||
"width": "1em",
|
||||
}
|
||||
}
|
||||
>
|
||||
<svg
|
||||
aria-hidden={true}
|
||||
fill="none"
|
||||
focusable={false}
|
||||
height={24}
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width={24}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M19 19H5V5h7V3H3v18h18v-9h-2v7ZM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
className="sr-only"
|
||||
>
|
||||
in a new tab
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</p>
|
||||
<div
|
||||
id="demographics-fields"
|
||||
>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Gender identity
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Race/Ethnicity identity
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Hispanic, Latin, or Spanish origin, White
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Family income
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
U.S. Military status
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Your education level
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Parents/Guardians education level
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Employment status
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Current work industry
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Future work industry
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`DemographicsSection should render ethnicity text correctly 1`] = `
|
||||
<div
|
||||
className="account-section pt-3 mb-5"
|
||||
id="demographics-information"
|
||||
>
|
||||
<h2
|
||||
className="section-heading h4 mb-3"
|
||||
>
|
||||
Optional Information
|
||||
</h2>
|
||||
<p>
|
||||
<a
|
||||
className="pgn__hyperlink default-link standalone-link"
|
||||
href="http://localhost:5335/demographics"
|
||||
onClick={[Function]}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Why does localhost collect this information?
|
||||
<span
|
||||
className="pgn__hyperlink__external-icon"
|
||||
title="Opens in a new tab"
|
||||
>
|
||||
<span
|
||||
className="pgn__icon"
|
||||
style={
|
||||
Object {
|
||||
"height": "1em",
|
||||
"width": "1em",
|
||||
}
|
||||
}
|
||||
>
|
||||
<svg
|
||||
aria-hidden={true}
|
||||
fill="none"
|
||||
focusable={false}
|
||||
height={24}
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width={24}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M19 19H5V5h7V3H3v18h18v-9h-2v7ZM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
className="sr-only"
|
||||
>
|
||||
in a new tab
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</p>
|
||||
<div
|
||||
id="demographics-fields"
|
||||
>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Gender identity
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Race/Ethnicity identity
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Asian
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Family income
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
U.S. Military status
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Your education level
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Parents/Guardians education level
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Employment status
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Current work industry
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Future work industry
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`DemographicsSection should set user input correctly when user provides answers to work_status question 1`] = `
|
||||
<div
|
||||
className="account-section pt-3 mb-5"
|
||||
id="demographics-information"
|
||||
>
|
||||
<h2
|
||||
className="section-heading h4 mb-3"
|
||||
>
|
||||
Optional Information
|
||||
</h2>
|
||||
<p>
|
||||
<a
|
||||
className="pgn__hyperlink default-link standalone-link"
|
||||
href="http://localhost:5335/demographics"
|
||||
onClick={[Function]}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Why does localhost collect this information?
|
||||
<span
|
||||
className="pgn__hyperlink__external-icon"
|
||||
title="Opens in a new tab"
|
||||
>
|
||||
<span
|
||||
className="pgn__icon"
|
||||
style={
|
||||
Object {
|
||||
"height": "1em",
|
||||
"width": "1em",
|
||||
}
|
||||
}
|
||||
>
|
||||
<svg
|
||||
aria-hidden={true}
|
||||
fill="none"
|
||||
focusable={false}
|
||||
height={24}
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width={24}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M19 19H5V5h7V3H3v18h18v-9h-2v7ZM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
className="sr-only"
|
||||
>
|
||||
in a new tab
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</p>
|
||||
<div
|
||||
id="demographics-fields"
|
||||
>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Gender identity
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Race/Ethnicity identity
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
<button
|
||||
className="p-0 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Add race/ethnicity identity
|
||||
</button>
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Family income
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
U.S. Military status
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Your education level
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Parents/Guardians education level
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Employment status
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Other: test
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Current work industry
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Future work industry
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`DemographicsSection should set user input correctly when user provides gender self-description 1`] = `
|
||||
<div
|
||||
className="account-section pt-3 mb-5"
|
||||
id="demographics-information"
|
||||
>
|
||||
<h2
|
||||
className="section-heading h4 mb-3"
|
||||
>
|
||||
Optional Information
|
||||
</h2>
|
||||
<p>
|
||||
<a
|
||||
className="pgn__hyperlink default-link standalone-link"
|
||||
href="http://localhost:5335/demographics"
|
||||
onClick={[Function]}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Why does localhost collect this information?
|
||||
<span
|
||||
className="pgn__hyperlink__external-icon"
|
||||
title="Opens in a new tab"
|
||||
>
|
||||
<span
|
||||
className="pgn__icon"
|
||||
style={
|
||||
Object {
|
||||
"height": "1em",
|
||||
"width": "1em",
|
||||
}
|
||||
}
|
||||
>
|
||||
<svg
|
||||
aria-hidden={true}
|
||||
fill="none"
|
||||
focusable={false}
|
||||
height={24}
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width={24}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M19 19H5V5h7V3H3v18h18v-9h-2v7ZM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
className="sr-only"
|
||||
>
|
||||
in a new tab
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</p>
|
||||
<div
|
||||
id="demographics-fields"
|
||||
>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Gender identity
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer to self describe: test
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Race/Ethnicity identity
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
<button
|
||||
className="p-0 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Add race/ethnicity identity
|
||||
</button>
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Family income
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
U.S. Military status
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Your education level
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Parents/Guardians education level
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Employment status
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Current work industry
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Future work industry
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { withLocation, withNavigate } from './hoc';
|
||||
|
||||
const mockedNavigator = jest.fn();
|
||||
@@ -15,24 +15,24 @@ jest.mock('react-router-dom', () => ({
|
||||
// eslint-disable-next-line react/prop-types
|
||||
const MockComponent = ({ navigate, location }) => (
|
||||
// eslint-disable-next-line react/button-has-type, react/prop-types
|
||||
<button id="btn" onClick={() => navigate('/some-route')}>{location}</button>
|
||||
<button data-testid="btn" onClick={() => navigate('/some-route')}>{location}</button>
|
||||
);
|
||||
const WrappedComponent = withNavigate(withLocation(MockComponent));
|
||||
|
||||
test('Provide Navigation to Component', () => {
|
||||
const wrapper = mount(
|
||||
render(
|
||||
<WrappedComponent />,
|
||||
);
|
||||
const btn = wrapper.find('#btn');
|
||||
btn.simulate('click');
|
||||
const btn = screen.getByTestId('btn');
|
||||
fireEvent.click(btn);
|
||||
|
||||
expect(mockedNavigator).toHaveBeenCalledWith('/some-route');
|
||||
});
|
||||
|
||||
test('Provide Location Pathname to Component', () => {
|
||||
const wrapper = mount(
|
||||
render(
|
||||
<WrappedComponent />,
|
||||
);
|
||||
|
||||
expect(wrapper.find('#btn').text()).toContain('/current-location');
|
||||
expect(screen.getByTestId('btn').textContent).toContain('/current-location');
|
||||
});
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { connect, useDispatch } from 'react-redux';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
ActionRow,
|
||||
Alert,
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
Form,
|
||||
ModalDialog,
|
||||
StatefulButton,
|
||||
} from '@edx/paragon';
|
||||
} from '@openedx/paragon';
|
||||
|
||||
import { closeForm, saveSettingsReset } from '../data/actions';
|
||||
import { nameChangeSelector } from '../data/selectors';
|
||||
@@ -25,7 +25,6 @@ const NameChangeModal = ({
|
||||
targetFormId,
|
||||
errors,
|
||||
formValues,
|
||||
intl,
|
||||
saveState,
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -33,6 +32,7 @@ const NameChangeModal = ({
|
||||
const { username } = getAuthenticatedUser();
|
||||
const [verifiedNameInput, setVerifiedNameInput] = useState(formValues.verified_name || '');
|
||||
const [confirmedWarning, setConfirmedWarning] = useState(false);
|
||||
const intl = useIntl();
|
||||
|
||||
const resetLocalState = useCallback(() => {
|
||||
setConfirmedWarning(false);
|
||||
@@ -193,11 +193,10 @@ NameChangeModal.propTypes = {
|
||||
verified_name: PropTypes.string,
|
||||
}).isRequired,
|
||||
saveState: PropTypes.string,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
NameChangeModal.defaultProps = {
|
||||
saveState: null,
|
||||
};
|
||||
|
||||
export default connect(nameChangeSelector)(injectIntl(NameChangeModal));
|
||||
export default connect(nameChangeSelector)(NameChangeModal);
|
||||
|
||||
56
src/account-settings/name-change/data/service.test.js
Normal file
56
src/account-settings/name-change/data/service.test.js
Normal file
@@ -0,0 +1,56 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { handleRequestError } from '../../data/utils';
|
||||
|
||||
import { postNameChange } from './service';
|
||||
|
||||
jest.mock('@edx/frontend-platform');
|
||||
jest.mock('@edx/frontend-platform/auth');
|
||||
jest.mock('../../data/utils');
|
||||
|
||||
describe('postNameChange', () => {
|
||||
const mockPost = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
|
||||
getConfig.mockReturnValue({
|
||||
LMS_BASE_URL: 'http://testserver',
|
||||
});
|
||||
|
||||
getAuthenticatedHttpClient.mockReturnValue({
|
||||
post: mockPost,
|
||||
});
|
||||
});
|
||||
|
||||
it('posts a name change request successfully', async () => {
|
||||
const mockResponse = { data: { success: true, updated: true } };
|
||||
mockPost.mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const result = await postNameChange('New Name');
|
||||
|
||||
expect(getConfig).toHaveBeenCalled();
|
||||
expect(getAuthenticatedHttpClient).toHaveBeenCalled();
|
||||
|
||||
expect(mockPost).toHaveBeenCalledWith(
|
||||
'http://testserver/api/user/v1/accounts/name_change/',
|
||||
{ name: 'New Name' },
|
||||
{ headers: { Accept: 'application/json' } },
|
||||
);
|
||||
|
||||
expect(result).toEqual(mockResponse.data);
|
||||
});
|
||||
|
||||
it('calls handleRequestError and throws when request fails', async () => {
|
||||
const mockError = new Error('Request failed');
|
||||
mockPost.mockRejectedValueOnce(mockError);
|
||||
|
||||
handleRequestError.mockImplementation(() => {
|
||||
throw mockError;
|
||||
});
|
||||
|
||||
await expect(postNameChange('Bad Name')).rejects.toThrow('Request failed');
|
||||
|
||||
expect(handleRequestError).toHaveBeenCalledWith(mockError);
|
||||
});
|
||||
});
|
||||
@@ -23,7 +23,7 @@ const messages = defineMessages({
|
||||
},
|
||||
'account.settings.name.change.id.name.label': {
|
||||
id: 'account.settings.name.change.id.name.label',
|
||||
defaultMessage: 'Enter your name as it appears on your unexpired student, work, or government-issued identification card.',
|
||||
defaultMessage: 'Enter your name as it appears on your identification card.',
|
||||
description: 'Form label instructing the user to enter the name on their ID.',
|
||||
},
|
||||
'account.settings.name.change.id.name.placeholder': {
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
/* eslint-disable no-import-assign */
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import configureStore from 'redux-mock-store';
|
||||
@@ -11,10 +9,13 @@ import {
|
||||
} from '@testing-library/react';
|
||||
|
||||
import * as auth from '@edx/frontend-platform/auth';
|
||||
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
|
||||
// Modal creates a portal. Overriding ReactDOM.createPortal allows portals to be tested in jest.
|
||||
ReactDOM.createPortal = node => node;
|
||||
// Modal creates a portal. Overriding createPortal allows portals to be tested in jest.
|
||||
jest.mock('react-dom', () => ({
|
||||
...jest.requireActual('react-dom'),
|
||||
createPortal: jest.fn(node => node), // Mock portal behavior
|
||||
}));
|
||||
|
||||
import NameChange from '../NameChange'; // eslint-disable-line import/first
|
||||
|
||||
@@ -27,8 +28,6 @@ jest.mock('react-redux', () => ({
|
||||
jest.mock('@edx/frontend-platform/auth');
|
||||
jest.mock('../../data/selectors', () => jest.fn().mockImplementation(() => ({ nameChangeSelector: () => ({}) })));
|
||||
|
||||
const IntlNameChange = injectIntl(NameChange);
|
||||
|
||||
const mockStore = configureStore();
|
||||
|
||||
describe('NameChange', () => {
|
||||
@@ -53,7 +52,6 @@ describe('NameChange', () => {
|
||||
verified_name: 'edX Verified',
|
||||
},
|
||||
saveState: null,
|
||||
intl: {},
|
||||
};
|
||||
|
||||
auth.getAuthenticatedHttpClient = jest.fn(() => ({
|
||||
@@ -70,7 +68,7 @@ describe('NameChange', () => {
|
||||
it('renders populated input after clicking continue if verified_name in form data', async () => {
|
||||
const getInput = () => screen.queryByPlaceholderText('Enter the name on your photo ID');
|
||||
|
||||
render(reduxWrapper(<IntlNameChange {...props} />));
|
||||
render(reduxWrapper(<NameChange {...props} />));
|
||||
expect(getInput()).toBeNull();
|
||||
|
||||
const continueButton = screen.getByText('Continue');
|
||||
@@ -87,7 +85,7 @@ describe('NameChange', () => {
|
||||
name: 'edx edx',
|
||||
},
|
||||
};
|
||||
render(reduxWrapper(<IntlNameChange {...formProps} />));
|
||||
render(reduxWrapper(<NameChange {...formProps} />));
|
||||
|
||||
const continueButton = screen.getByText('Continue');
|
||||
fireEvent.click(continueButton);
|
||||
@@ -105,7 +103,7 @@ describe('NameChange', () => {
|
||||
type: 'ACCOUNT_SETTINGS__REQUEST_NAME_CHANGE',
|
||||
};
|
||||
|
||||
render(reduxWrapper(<IntlNameChange {...props} />));
|
||||
render(reduxWrapper(<NameChange {...props} />));
|
||||
|
||||
const continueButton = screen.getByText('Continue');
|
||||
fireEvent.click(continueButton);
|
||||
@@ -132,7 +130,7 @@ describe('NameChange', () => {
|
||||
targetFormId: 'name',
|
||||
};
|
||||
|
||||
render(reduxWrapper(<IntlNameChange {...formProps} />));
|
||||
render(reduxWrapper(<NameChange {...formProps} />));
|
||||
|
||||
const continueButton = screen.getByText('Continue');
|
||||
fireEvent.click(continueButton);
|
||||
@@ -148,7 +146,7 @@ describe('NameChange', () => {
|
||||
it('does not dispatch action while pending', async () => {
|
||||
props.saveState = 'pending';
|
||||
|
||||
render(reduxWrapper(<IntlNameChange {...props} />));
|
||||
render(reduxWrapper(<NameChange {...props} />));
|
||||
|
||||
const continueButton = screen.getByText('Continue');
|
||||
fireEvent.click(continueButton);
|
||||
@@ -164,7 +162,7 @@ describe('NameChange', () => {
|
||||
it('routes to IDV when name change request is successful', async () => {
|
||||
props.saveState = 'complete';
|
||||
|
||||
render(reduxWrapper(<IntlNameChange {...props} />));
|
||||
render(reduxWrapper(<NameChange {...props} />));
|
||||
expect(window.location.pathname).toEqual('/id-verification');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink } from '@edx/paragon';
|
||||
import { Hyperlink } from '@openedx/paragon';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { StatefulButton } from '@edx/paragon';
|
||||
import { useIntl, FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { StatefulButton } from '@openedx/paragon';
|
||||
|
||||
import { resetPassword } from './data/actions';
|
||||
import messages from './messages';
|
||||
@@ -10,7 +9,9 @@ import ConfirmationAlert from './ConfirmationAlert';
|
||||
import RequestInProgressAlert from './RequestInProgressAlert';
|
||||
|
||||
const ResetPassword = (props) => {
|
||||
const { email, intl, status } = props;
|
||||
const { email, status } = props;
|
||||
const intl = useIntl();
|
||||
|
||||
return (
|
||||
<div className="form-group">
|
||||
<h6 aria-level="3">
|
||||
@@ -51,7 +52,6 @@ const ResetPassword = (props) => {
|
||||
|
||||
ResetPassword.propTypes = {
|
||||
email: PropTypes.string,
|
||||
intl: intlShape.isRequired,
|
||||
resetPassword: PropTypes.func.isRequired,
|
||||
status: PropTypes.string,
|
||||
};
|
||||
@@ -68,4 +68,4 @@ export default connect(
|
||||
{
|
||||
resetPassword,
|
||||
},
|
||||
)(injectIntl(ResetPassword));
|
||||
)(ResetPassword);
|
||||
|
||||
65
src/account-settings/reset-password/data/service.test.js
Normal file
65
src/account-settings/reset-password/data/service.test.js
Normal file
@@ -0,0 +1,65 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import formurlencoded from 'form-urlencoded';
|
||||
import { handleRequestError } from '../../data/utils';
|
||||
|
||||
import { postResetPassword } from './service';
|
||||
|
||||
jest.mock('@edx/frontend-platform');
|
||||
jest.mock('@edx/frontend-platform/auth');
|
||||
jest.mock('form-urlencoded');
|
||||
jest.mock('../../data/utils');
|
||||
|
||||
describe('postResetPassword', () => {
|
||||
const mockPost = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
|
||||
getConfig.mockReturnValue({
|
||||
LMS_BASE_URL: 'http://testserver',
|
||||
});
|
||||
|
||||
getAuthenticatedHttpClient.mockReturnValue({
|
||||
post: mockPost,
|
||||
});
|
||||
|
||||
formurlencoded.mockImplementation(obj => `encoded:${JSON.stringify(obj)}`);
|
||||
});
|
||||
|
||||
it('posts reset password request with email', async () => {
|
||||
const mockResponse = { data: { success: true, email_sent: true } };
|
||||
mockPost.mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const result = await postResetPassword('user@example.com');
|
||||
|
||||
expect(getConfig).toHaveBeenCalled();
|
||||
expect(getAuthenticatedHttpClient).toHaveBeenCalled();
|
||||
expect(formurlencoded).toHaveBeenCalledWith({ email: 'user@example.com' });
|
||||
|
||||
expect(mockPost).toHaveBeenCalledWith(
|
||||
'http://testserver/password_reset/',
|
||||
'encoded:{"email":"user@example.com"}',
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).toEqual(mockResponse.data);
|
||||
});
|
||||
|
||||
it('calls handleRequestError and throws when request fails', async () => {
|
||||
const mockError = new Error('Reset password failed');
|
||||
mockPost.mockRejectedValueOnce(mockError);
|
||||
|
||||
handleRequestError.mockImplementation(() => {
|
||||
throw mockError;
|
||||
});
|
||||
|
||||
await expect(postResetPassword('bad@example.com')).rejects.toThrow('Reset password failed');
|
||||
|
||||
expect(handleRequestError).toHaveBeenCalledWith(mockError);
|
||||
});
|
||||
});
|
||||
@@ -75,17 +75,17 @@ const siteLanguageList = [
|
||||
released: true,
|
||||
},
|
||||
{
|
||||
code: 'pt',
|
||||
code: 'pt-pt',
|
||||
name: 'Português',
|
||||
released: true,
|
||||
},
|
||||
{
|
||||
code: 'it',
|
||||
code: 'it-it',
|
||||
name: 'Italian',
|
||||
released: true,
|
||||
},
|
||||
{
|
||||
code: 'de',
|
||||
code: 'de-de',
|
||||
name: 'German',
|
||||
released: true,
|
||||
},
|
||||
@@ -99,6 +99,36 @@ const siteLanguageList = [
|
||||
name: 'French (CA)',
|
||||
released: true,
|
||||
},
|
||||
{
|
||||
code: 'te',
|
||||
name: 'తెలుగు',
|
||||
released: true,
|
||||
},
|
||||
{
|
||||
code: 'da',
|
||||
name: 'dansk',
|
||||
released: true,
|
||||
},
|
||||
{
|
||||
code: 'el',
|
||||
name: 'Ελληνικά',
|
||||
released: true,
|
||||
},
|
||||
{
|
||||
code: 'es-es',
|
||||
name: 'Español (España)',
|
||||
released: true,
|
||||
},
|
||||
{
|
||||
code: 'sw',
|
||||
name: 'Kiswahili',
|
||||
released: true,
|
||||
},
|
||||
{
|
||||
code: 'tr-tr',
|
||||
name: 'Türkçe (Türkiye)',
|
||||
released: true,
|
||||
},
|
||||
];
|
||||
|
||||
export default siteLanguageList;
|
||||
|
||||
@@ -23,10 +23,15 @@ export async function patchPreferences(username, params) {
|
||||
|
||||
export async function postSetLang(code) {
|
||||
const formData = new FormData();
|
||||
const requestConfig = {
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
},
|
||||
};
|
||||
const url = `${getConfig().LMS_BASE_URL}/i18n/setlang/`;
|
||||
formData.append('language', code);
|
||||
|
||||
await getAuthenticatedHttpClient()
|
||||
.post(`${getConfig().LMS_BASE_URL}/i18n/setlang/`, formData, {
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' },
|
||||
});
|
||||
.post(url, formData, requestConfig);
|
||||
}
|
||||
|
||||
95
src/account-settings/site-language/service.test.js
Normal file
95
src/account-settings/site-language/service.test.js
Normal file
@@ -0,0 +1,95 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { convertKeyNames, snakeCaseObject } from '@edx/frontend-platform/utils';
|
||||
|
||||
import { getSiteLanguageList, patchPreferences, postSetLang } from './service';
|
||||
|
||||
jest.mock('@edx/frontend-platform');
|
||||
jest.mock('@edx/frontend-platform/auth');
|
||||
jest.mock('@edx/frontend-platform/utils');
|
||||
jest.mock('./constants', () => (['en', 'es', 'fr']));
|
||||
|
||||
describe('preferencesApi', () => {
|
||||
const mockPatch = jest.fn();
|
||||
const mockPost = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
|
||||
getConfig.mockReturnValue({
|
||||
LMS_BASE_URL: 'http://testserver',
|
||||
});
|
||||
|
||||
getAuthenticatedHttpClient.mockReturnValue({
|
||||
patch: mockPatch,
|
||||
post: mockPost,
|
||||
});
|
||||
|
||||
snakeCaseObject.mockImplementation(obj => obj);
|
||||
convertKeyNames.mockImplementation((obj) => obj);
|
||||
});
|
||||
|
||||
describe('getSiteLanguageList', () => {
|
||||
it('returns the siteLanguageList constant', async () => {
|
||||
const result = await getSiteLanguageList();
|
||||
expect(result).toEqual(['en', 'es', 'fr']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('patchPreferences', () => {
|
||||
it('patches preferences with processed params and returns the original params', async () => {
|
||||
const username = 'testuser';
|
||||
const params = { prefLang: 'en', darkMode: true };
|
||||
const processed = { 'pref-lang': 'en', dark_mode: true };
|
||||
|
||||
// Mock conversions
|
||||
snakeCaseObject.mockReturnValueOnce({ pref_lang: 'en', dark_mode: true });
|
||||
convertKeyNames.mockReturnValueOnce(processed);
|
||||
|
||||
mockPatch.mockResolvedValueOnce({ data: { success: true } });
|
||||
|
||||
const result = await patchPreferences(username, params);
|
||||
|
||||
expect(snakeCaseObject).toHaveBeenCalledWith(params);
|
||||
expect(convertKeyNames).toHaveBeenCalledWith(
|
||||
{ pref_lang: 'en', dark_mode: true },
|
||||
{ pref_lang: 'pref-lang' },
|
||||
);
|
||||
|
||||
expect(mockPatch).toHaveBeenCalledWith(
|
||||
'http://testserver/api/user/v1/preferences/testuser',
|
||||
processed,
|
||||
{
|
||||
headers: { 'Content-Type': 'application/merge-patch+json' },
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).toEqual(params);
|
||||
});
|
||||
});
|
||||
|
||||
describe('postSetLang', () => {
|
||||
it('posts language selection via FormData', async () => {
|
||||
const mockResponse = { data: { success: true } };
|
||||
mockPost.mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const appendSpy = jest.spyOn(FormData.prototype, 'append');
|
||||
|
||||
await postSetLang('fr');
|
||||
|
||||
expect(appendSpy).toHaveBeenCalledWith('language', 'fr');
|
||||
expect(mockPost).toHaveBeenCalledWith(
|
||||
'http://testserver/i18n/setlang/',
|
||||
expect.any(FormData),
|
||||
{
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
appendSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
});
|
||||
233
src/account-settings/test/AccountSettingsPage.test.jsx
Normal file
233
src/account-settings/test/AccountSettingsPage.test.jsx
Normal file
@@ -0,0 +1,233 @@
|
||||
import React from 'react';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import thunk from 'redux-thunk';
|
||||
import { AppContext } from '@edx/frontend-platform/react';
|
||||
import {
|
||||
render, screen, fireEvent,
|
||||
} from '@testing-library/react';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import AccountSettingsPage from '../AccountSettingsPage';
|
||||
import mockData from './mockData';
|
||||
|
||||
const mockDispatch = jest.fn();
|
||||
jest.mock('@edx/frontend-platform/analytics', () => ({
|
||||
sendTrackingLogEvent: jest.fn(),
|
||||
getCountryList: jest.fn(() => [{ code: 'US', name: 'United States' }]),
|
||||
}));
|
||||
|
||||
jest.mock('react-redux', () => ({
|
||||
...jest.requireActual('react-redux'),
|
||||
useDispatch: () => mockDispatch,
|
||||
}));
|
||||
|
||||
jest.mock('@edx/frontend-platform/auth');
|
||||
|
||||
jest.mock('@edx/frontend-platform', () => ({
|
||||
...jest.requireActual('@edx/frontend-platform'),
|
||||
getConfig: jest.fn(() => ({
|
||||
SITE_NAME: 'edX',
|
||||
SUPPORT_URL: 'https://support.edx.org',
|
||||
ENABLE_ACCOUNT_DELETION: true,
|
||||
ENABLE_COPPA_COMPLIANCE: false,
|
||||
COUNTRIES_WITH_DELETE_ACCOUNT_DISABLED: [],
|
||||
})),
|
||||
getCountryList: jest.fn(() => [{ code: 'US', name: 'United States' }]),
|
||||
getLanguageList: jest.fn(() => [{ code: 'en', name: 'English' }]),
|
||||
}));
|
||||
|
||||
const IntlAccountSettingsPage = injectIntl(AccountSettingsPage);
|
||||
|
||||
const middlewares = [thunk];
|
||||
const mockStore = configureStore(middlewares);
|
||||
|
||||
describe('AccountSettingsPage', () => {
|
||||
let props = {};
|
||||
let store = {};
|
||||
const appContext = { locale: 'en', authenticatedUser: { userId: 3, roles: [] } };
|
||||
const reduxWrapper = children => (
|
||||
<AppContext.Provider value={appContext}>
|
||||
<Router>
|
||||
<IntlProvider locale="en">
|
||||
<Provider store={store}>
|
||||
{children}
|
||||
</Provider>
|
||||
</IntlProvider>
|
||||
</Router>
|
||||
</AppContext.Provider>
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
store = mockStore(mockData);
|
||||
props = {
|
||||
loaded: true,
|
||||
siteLanguage: {},
|
||||
formValues: {
|
||||
username: 'test_username',
|
||||
accomplishments_shared: false,
|
||||
name: 'test_name',
|
||||
email: 'test_email@test.com',
|
||||
id: 534,
|
||||
extended_profile: [
|
||||
{
|
||||
field_name: 'work_experience',
|
||||
field_value: '',
|
||||
},
|
||||
],
|
||||
country: 'US',
|
||||
level_of_education: 'b',
|
||||
gender: 'm',
|
||||
language_proficiencies: 'es',
|
||||
social_link_linkedin: 'https://linkedin.com/in/testuser',
|
||||
social_link_facebook: '',
|
||||
social_link_twitter: '',
|
||||
time_zone: 'America/New_York',
|
||||
state: 'NY',
|
||||
secondary_email_enabled: true,
|
||||
secondary_email: 'test_recovery@test.com',
|
||||
year_of_birth: '1990',
|
||||
},
|
||||
fetchSettings: jest.fn(),
|
||||
fetchSiteLanguages: jest.fn(),
|
||||
fetchNotificationPreferences: jest.fn(),
|
||||
saveSettings: jest.fn(),
|
||||
updateDraft: jest.fn(),
|
||||
beginNameChange: jest.fn(),
|
||||
saveMultipleSettings: jest.fn(),
|
||||
timeZoneOptions: [
|
||||
{ label: 'America/New_York', value: 'America/New_York' },
|
||||
],
|
||||
countryTimeZoneOptions: [
|
||||
{ label: 'America/New_York', value: 'America/New_York' },
|
||||
],
|
||||
siteLanguageOptions: [
|
||||
{ label: 'English', value: 'en' },
|
||||
],
|
||||
tpaProviders: [
|
||||
{
|
||||
id: 'oa2-google-oauth2',
|
||||
name: 'Google',
|
||||
connected: false,
|
||||
accepts_logins: true,
|
||||
connectUrl: 'http://localhost:18000/auth/login/google-oauth2/',
|
||||
disconnectUrl: 'http://localhost:18000/auth/disconnect/google-oauth2/',
|
||||
},
|
||||
],
|
||||
isActive: true,
|
||||
staticFields: [],
|
||||
profileDataManager: null,
|
||||
verifiedName: null,
|
||||
mostRecentVerifiedName: {},
|
||||
verifiedNameHistory: [],
|
||||
countriesCodesList: ['US'],
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => jest.clearAllMocks());
|
||||
|
||||
beforeAll(() => {
|
||||
global.lightningjs = {
|
||||
require: jest.fn().mockImplementation((module, url) => ({ moduleName: module, url })),
|
||||
};
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
delete global.lightningjs;
|
||||
});
|
||||
|
||||
it('renders AccountSettingsPage correctly with editing enabled', async () => {
|
||||
const { getByText, rerender, getByLabelText } = render(reduxWrapper(<IntlAccountSettingsPage {...props} />));
|
||||
|
||||
const workExperienceText = getByText('Work Experience');
|
||||
const workExperienceEditButton = workExperienceText.parentElement.querySelector('button');
|
||||
|
||||
expect(workExperienceEditButton).toBeInTheDocument();
|
||||
|
||||
store = mockStore({
|
||||
...mockData,
|
||||
accountSettings: {
|
||||
...mockData.accountSettings,
|
||||
openFormId: 'work_experience',
|
||||
},
|
||||
});
|
||||
rerender(reduxWrapper(<IntlAccountSettingsPage {...props} />));
|
||||
|
||||
const submitButton = screen.getByText('Save');
|
||||
expect(submitButton).toBeInTheDocument();
|
||||
|
||||
const workExperienceSelect = getByLabelText('Work Experience');
|
||||
|
||||
// Use fireEvent.change to simulate changing the selected value
|
||||
fireEvent.change(workExperienceSelect, { target: { value: '4' } });
|
||||
|
||||
fireEvent.click(submitButton);
|
||||
});
|
||||
|
||||
it('renders Account Information section with correct field values', () => {
|
||||
render(reduxWrapper(<AccountSettingsPage {...props} />));
|
||||
|
||||
expect(screen.getByText('test_username')).toBeInTheDocument();
|
||||
expect(screen.getByText('test_name')).toBeInTheDocument();
|
||||
expect(screen.getByText('test_email@test.com')).toBeInTheDocument();
|
||||
expect(screen.getByText('test_recovery@test.com')).toBeInTheDocument();
|
||||
expect(screen.getByText('1990')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders Profile Information section with correct field values', () => {
|
||||
render(reduxWrapper(<AccountSettingsPage {...props} />));
|
||||
|
||||
expect(screen.getByText('Bachelor\'s Degree')).toBeInTheDocument();
|
||||
expect(screen.getByText('Male')).toBeInTheDocument();
|
||||
expect(screen.getByText('Add work experience')).toBeInTheDocument();
|
||||
expect(screen.getByText('English')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders Social Media section with correct field values', () => {
|
||||
render(reduxWrapper(<AccountSettingsPage {...props} />));
|
||||
|
||||
expect(screen.getByText('https://linkedin.com/in/testuser')).toBeInTheDocument();
|
||||
expect(screen.getByText('Add Facebook profile')).toBeInTheDocument();
|
||||
expect(screen.getByText('Add Twitter profile')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders Site Preferences section with correct field values', () => {
|
||||
render(reduxWrapper(<AccountSettingsPage {...props} />));
|
||||
|
||||
expect(screen.getByText('English')).toBeInTheDocument();
|
||||
expect(screen.getByText('America/New_York')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders Delete Account section when enabled', () => {
|
||||
// eslint-disable-next-line global-require
|
||||
const { getConfig } = require('@edx/frontend-platform');
|
||||
jest.spyOn({ getConfig }, 'getConfig').mockImplementation(() => ({
|
||||
SITE_NAME: 'edX',
|
||||
SUPPORT_URL: 'https://support.edx.org',
|
||||
ENABLE_ACCOUNT_DELETION: true,
|
||||
ENABLE_COPPA_COMPLIANCE: false,
|
||||
COUNTRIES_WITH_DELETE_ACCOUNT_DISABLED: [],
|
||||
}));
|
||||
|
||||
render(reduxWrapper(<AccountSettingsPage {...props} />));
|
||||
|
||||
expect(screen.getByText('We\'re sorry to see you go!')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render Delete Account section when disabled', () => {
|
||||
// eslint-disable-next-line global-require
|
||||
const { getConfig } = require('@edx/frontend-platform');
|
||||
jest.spyOn({ getConfig }, 'getConfig').mockImplementation(() => ({
|
||||
SITE_NAME: 'edX',
|
||||
SUPPORT_URL: 'https://support.edx.org',
|
||||
ENABLE_ACCOUNT_DELETION: false,
|
||||
ENABLE_COPPA_COMPLIANCE: false,
|
||||
COUNTRIES_WITH_DELETE_ACCOUNT_DISABLED: [],
|
||||
}));
|
||||
|
||||
render(reduxWrapper(<AccountSettingsPage {...props} />));
|
||||
|
||||
expect(screen.queryByText('We\'re sorry to see you go!')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
144
src/account-settings/test/DOBForm.test.jsx
Normal file
144
src/account-settings/test/DOBForm.test.jsx
Normal file
@@ -0,0 +1,144 @@
|
||||
import {
|
||||
render, screen, fireEvent, waitFor,
|
||||
} from '@testing-library/react';
|
||||
import { Provider } from 'react-redux';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import * as reactRedux from 'react-redux';
|
||||
import DOBModal from '../DOBForm';
|
||||
import messages from '../AccountSettingsPage.messages';
|
||||
import { YEAR_OF_BIRTH_OPTIONS } from '../data/constants';
|
||||
|
||||
jest.mock('react-redux', () => ({
|
||||
...jest.requireActual('react-redux'),
|
||||
useDispatch: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@edx/frontend-platform/i18n', () => ({
|
||||
...jest.requireActual('@edx/frontend-platform/i18n'),
|
||||
useIntl: () => ({
|
||||
formatMessage: (message) => message.defaultMessage,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('@openedx/paragon', () => ({
|
||||
...jest.requireActual('@openedx/paragon'),
|
||||
Form: {
|
||||
...jest.requireActual('@openedx/paragon').Form,
|
||||
Control: {
|
||||
...jest.requireActual('@openedx/paragon').Form.Control,
|
||||
// eslint-disable-next-line react/prop-types
|
||||
Feedback: ({ children, ...props }) => <div {...props}>{children}</div>,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const mockStore = configureStore([]);
|
||||
|
||||
describe('DOBModal', () => {
|
||||
let store;
|
||||
let mockDispatch;
|
||||
|
||||
beforeEach(() => {
|
||||
store = mockStore({
|
||||
accountSettings: {
|
||||
saveState: 'default',
|
||||
errors: {},
|
||||
openFormId: null,
|
||||
confirmationValues: {},
|
||||
},
|
||||
});
|
||||
mockDispatch = jest.fn();
|
||||
jest.spyOn(reactRedux, 'useDispatch').mockReturnValue(mockDispatch); // ✅ replaced require with import
|
||||
// Mock localStorage.setItem
|
||||
Object.defineProperty(window, 'localStorage', {
|
||||
value: {
|
||||
setItem: jest.fn(),
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const renderComponent = (props = {}) => render(
|
||||
<Provider store={store}>
|
||||
<IntlProvider locale="en">
|
||||
<DOBModal
|
||||
saveState="default"
|
||||
error={undefined}
|
||||
onSubmit={jest.fn()}
|
||||
{...props}
|
||||
/>
|
||||
</IntlProvider>
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
it('renders the modal with correct elements', async () => {
|
||||
renderComponent();
|
||||
const openButton = screen.getByTestId('open-modal-button');
|
||||
expect(openButton).toHaveTextContent(messages['account.settings.field.dob.form.button'].defaultMessage);
|
||||
|
||||
fireEvent.click(openButton);
|
||||
|
||||
expect(screen.getByTestId('modal-title')).toHaveTextContent(messages['account.settings.field.dob.form.title'].defaultMessage);
|
||||
expect(screen.getByTestId('help-text')).toHaveTextContent(messages['account.settings.field.dob.form.help.text'].defaultMessage);
|
||||
expect(screen.getByTestId('month-label')).toHaveTextContent(messages['account.settings.field.dob.month'].defaultMessage);
|
||||
expect(screen.getByTestId('year-label')).toHaveTextContent(messages['account.settings.field.dob.year'].defaultMessage);
|
||||
expect(screen.getByTestId('month-select')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('year-select')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('cancel-button')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('submit-button')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('enables submit button when both month and year are selected', async () => {
|
||||
renderComponent();
|
||||
const openButton = screen.getByTestId('open-modal-button');
|
||||
await act(async () => {
|
||||
fireEvent.click(openButton);
|
||||
});
|
||||
await waitFor(() => {
|
||||
const monthSelect = screen.getByTestId('month-select');
|
||||
const yearSelect = screen.getByTestId('year-select');
|
||||
const submitButton = screen.getByTestId('submit-button');
|
||||
|
||||
act(() => {
|
||||
fireEvent.change(monthSelect, { target: { value: '6' } });
|
||||
fireEvent.change(yearSelect, { target: { value: YEAR_OF_BIRTH_OPTIONS[0].value } });
|
||||
});
|
||||
|
||||
expect(submitButton).not.toHaveAttribute('disabled');
|
||||
}, { timeout: 2000 });
|
||||
});
|
||||
|
||||
it('calls onSubmit with correct data when form is submitted', async () => {
|
||||
const mockOnSubmit = jest.fn();
|
||||
renderComponent({ onSubmit: mockOnSubmit });
|
||||
|
||||
const openButton = screen.getByTestId('open-modal-button');
|
||||
await act(async () => {
|
||||
fireEvent.click(openButton);
|
||||
});
|
||||
await waitFor(() => {
|
||||
const monthSelect = screen.getByTestId('month-select');
|
||||
const yearSelect = screen.getByTestId('year-select');
|
||||
const form = screen.getByTestId('dob-form');
|
||||
|
||||
act(() => {
|
||||
fireEvent.change(monthSelect, { target: { value: '6' } });
|
||||
fireEvent.change(yearSelect, { target: { value: '1990' } });
|
||||
});
|
||||
|
||||
act(() => {
|
||||
fireEvent.submit(form);
|
||||
});
|
||||
|
||||
expect(mockOnSubmit).toHaveBeenCalledWith('extended_profile', [
|
||||
{ field_name: 'DOB', field_value: '1990-6' },
|
||||
]);
|
||||
}, { timeout: 2000 });
|
||||
});
|
||||
});
|
||||
184
src/account-settings/test/EditableField.test.jsx
Normal file
184
src/account-settings/test/EditableField.test.jsx
Normal file
@@ -0,0 +1,184 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
render, screen, fireEvent, waitFor,
|
||||
} from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import { Provider } from 'react-redux';
|
||||
import EditableField from '../EditableField';
|
||||
import messages from '../AccountSettingsPage.messages';
|
||||
|
||||
jest.mock('../data/selectors', () => ({
|
||||
editableFieldSelector: () => (state, props) => ({
|
||||
...state.accountSettings,
|
||||
isEditing: props.isEditing,
|
||||
error: props.error || state.accountSettings.errors[props.name],
|
||||
confirmationValue: props.confirmationValue || state.accountSettings.confirmationValues[props.name],
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('../data/actions', () => ({
|
||||
openForm: jest.fn((name) => ({ type: 'OPEN_FORM', payload: name })),
|
||||
closeForm: jest.fn((name) => ({ type: 'CLOSE_FORM', payload: name })),
|
||||
}));
|
||||
|
||||
// eslint-disable-next-line react/prop-types
|
||||
jest.mock('../certificate-preference/CertificatePreference', () => function MockCertificatePreference({ fieldName }) {
|
||||
return <div data-testid="editable-field-certificate-preference">Certificate Preference for {fieldName}</div>;
|
||||
});
|
||||
|
||||
const mockStore = configureStore([]);
|
||||
const mockOnEdit = jest.fn();
|
||||
const mockOnCancel = jest.fn();
|
||||
const mockOnSubmit = jest.fn();
|
||||
const mockOnChange = jest.fn();
|
||||
|
||||
const baseState = {
|
||||
accountSettings: {
|
||||
errors: {},
|
||||
confirmationValues: {},
|
||||
saveState: 'default',
|
||||
openFormId: null,
|
||||
verifiedNameHistory: { results: [] },
|
||||
values: {},
|
||||
drafts: {},
|
||||
timeZones: [],
|
||||
countryTimeZones: [],
|
||||
thirdPartyAuth: { providers: [] },
|
||||
countriesCodesList: [],
|
||||
profileDataManager: false,
|
||||
nameChangeModal: {},
|
||||
loading: false,
|
||||
loaded: true,
|
||||
loadingError: null,
|
||||
},
|
||||
};
|
||||
|
||||
const renderComponent = (props = {}, stateOverrides = {}) => {
|
||||
const store = mockStore({
|
||||
...baseState,
|
||||
...stateOverrides,
|
||||
});
|
||||
return render(
|
||||
<Provider store={store}>
|
||||
<IntlProvider locale="en">
|
||||
<EditableField
|
||||
name="username"
|
||||
label="Username"
|
||||
type="text"
|
||||
value="john_doe"
|
||||
onEdit={mockOnEdit}
|
||||
onCancel={mockOnCancel}
|
||||
onSubmit={mockOnSubmit}
|
||||
onChange={mockOnChange}
|
||||
isEditing={false}
|
||||
{...props}
|
||||
/>
|
||||
</IntlProvider>
|
||||
</Provider>,
|
||||
);
|
||||
};
|
||||
|
||||
describe('EditableField', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders default state with value', () => {
|
||||
renderComponent();
|
||||
expect(screen.getByText('Username')).toBeInTheDocument();
|
||||
expect(screen.getByText('john_doe')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /Edit/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders empty label with edit button if no value and editable', () => {
|
||||
renderComponent({ value: '', emptyLabel: 'Add value' });
|
||||
expect(screen.getByRole('button', { name: 'Add value' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders empty label as muted text if not editable', () => {
|
||||
renderComponent({ value: '', emptyLabel: 'No value', isEditable: false });
|
||||
expect(screen.getByText('No value')).toHaveClass('text-muted');
|
||||
});
|
||||
|
||||
it('renders editing state with form controls', async () => {
|
||||
renderComponent({ isEditing: true });
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('editable-field-textbox')).toHaveValue('john_doe');
|
||||
expect(screen.getByTestId('editable-field-save')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('editable-field-cancel')).toBeInTheDocument();
|
||||
}, { timeout: 2000 });
|
||||
});
|
||||
|
||||
it('calls onChange when input changes', async () => {
|
||||
renderComponent({ isEditing: true });
|
||||
await waitFor(() => {
|
||||
const input = screen.getByTestId('editable-field-textbox');
|
||||
fireEvent.change(input, { target: { value: 'new_name' } });
|
||||
expect(mockOnChange).toHaveBeenCalledWith('username', 'new_name');
|
||||
}, { timeout: 2000 });
|
||||
});
|
||||
|
||||
it('calls onSubmit when form is submitted', async () => {
|
||||
renderComponent({ isEditing: true });
|
||||
await waitFor(() => {
|
||||
const form = screen.getByTestId('editable-field-form');
|
||||
fireEvent.submit(form);
|
||||
expect(mockOnSubmit).toHaveBeenCalledWith('username', 'john_doe');
|
||||
}, { timeout: 2000 });
|
||||
});
|
||||
|
||||
it('shows error message when error is present', async () => {
|
||||
const stateOverrides = {
|
||||
accountSettings: {
|
||||
...baseState.accountSettings,
|
||||
errors: { username: 'Invalid input' },
|
||||
},
|
||||
};
|
||||
renderComponent({ isEditing: true, error: 'Invalid input' }, stateOverrides);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('editable-field-error')).toHaveTextContent('Invalid input');
|
||||
}, { timeout: 2000 });
|
||||
});
|
||||
|
||||
it('shows help text in editing mode', () => {
|
||||
renderComponent({ isEditing: true, helpText: 'Helpful info' });
|
||||
expect(screen.getByText('Helpful info')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows confirmation message in default mode if provided', async () => {
|
||||
const stateOverrides = {
|
||||
accountSettings: {
|
||||
...baseState.accountSettings,
|
||||
confirmationValues: { username: 'done' },
|
||||
},
|
||||
};
|
||||
renderComponent(
|
||||
{
|
||||
confirmationMessageDefinition: messages['account.settings.editable.field.action.save'],
|
||||
confirmationValue: 'done',
|
||||
},
|
||||
stateOverrides,
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('editable-field-confirmation')).toBeInTheDocument();
|
||||
}, { timeout: 2000 });
|
||||
});
|
||||
|
||||
it('renders CertificatePreference for name fields when editing', async () => {
|
||||
renderComponent({ isEditing: true, name: 'name' });
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('editable-field-certificate-preference')).toHaveTextContent('Certificate Preference for name');
|
||||
}, { timeout: 2000 });
|
||||
});
|
||||
|
||||
it('applies grayed-out class when isGrayedOut is true', () => {
|
||||
renderComponent({ isGrayedOut: true });
|
||||
expect(screen.getByText('john_doe')).toHaveClass('grayed-out');
|
||||
});
|
||||
|
||||
it('appends userSuppliedValue when provided', () => {
|
||||
renderComponent({ userSuppliedValue: 'extra' });
|
||||
expect(screen.getByText('john_doe: extra')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
166
src/account-settings/test/EditableSelectField.test.jsx
Normal file
166
src/account-settings/test/EditableSelectField.test.jsx
Normal file
@@ -0,0 +1,166 @@
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import renderer from 'react-test-renderer';
|
||||
import configureStore from 'redux-mock-store';
|
||||
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import EditableSelectField from '../EditableSelectField';
|
||||
|
||||
const mockDispatch = jest.fn();
|
||||
jest.mock('react-redux', () => ({
|
||||
...jest.requireActual('react-redux'),
|
||||
useDispatch: () => mockDispatch,
|
||||
}));
|
||||
|
||||
jest.mock('@edx/frontend-platform/auth');
|
||||
jest.mock('../data/selectors', () => jest.fn().mockImplementation(() => ({ certPreferenceSelector: () => ({}) })));
|
||||
|
||||
const mockStore = configureStore();
|
||||
|
||||
describe('EditableSelectField', () => {
|
||||
let props = {};
|
||||
let store = {};
|
||||
|
||||
const reduxWrapper = children => (
|
||||
<Router>
|
||||
<IntlProvider locale="en">
|
||||
<Provider store={store}>{children}</Provider>
|
||||
</IntlProvider>
|
||||
</Router>
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
store = mockStore();
|
||||
props = {
|
||||
name: 'testField',
|
||||
label: 'Main Label',
|
||||
emptyLabel: 'Empty Main Label',
|
||||
type: 'text',
|
||||
value: 'Test Field',
|
||||
userSuppliedValue: '',
|
||||
options: [
|
||||
{
|
||||
label: 'Default Option',
|
||||
value: 'defaultOption',
|
||||
},
|
||||
{
|
||||
label: 'User Options',
|
||||
group: [
|
||||
{
|
||||
label: 'Suboption 1',
|
||||
value: 'suboption1',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Other Options',
|
||||
group: [
|
||||
{
|
||||
label: 'Suboption 2',
|
||||
value: 'suboption2',
|
||||
},
|
||||
{
|
||||
label: 'Suboption 3',
|
||||
value: 'suboption3',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
saveState: 'default',
|
||||
error: '',
|
||||
confirmationMessageDefinition: {
|
||||
id: 'confirmationMessageId',
|
||||
defaultMessage: 'Default Confirmation Message',
|
||||
description: 'Description of the confirmation message',
|
||||
},
|
||||
confirmationValue: 'Confirmation Value',
|
||||
helpText: 'Helpful Text',
|
||||
isEditing: false,
|
||||
isEditable: true,
|
||||
isGrayedOut: false,
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => jest.clearAllMocks());
|
||||
|
||||
it('renders EditableSelectField correctly with editing disabled', () => {
|
||||
const tree = renderer.create(reduxWrapper(<EditableSelectField {...props} />)).toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders EditableSelectField correctly with editing enabled', () => {
|
||||
props = {
|
||||
...props,
|
||||
isEditing: true,
|
||||
};
|
||||
|
||||
const tree = renderer.create(reduxWrapper(<EditableSelectField {...props} />)).toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders EditableSelectField with an error', () => {
|
||||
const errorProps = {
|
||||
...props,
|
||||
error: 'This is an error message',
|
||||
};
|
||||
const tree = renderer.create(reduxWrapper(<EditableSelectField {...errorProps} />)).toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders selectOptions when option has a group', () => {
|
||||
const propsWithGroup = {
|
||||
...props,
|
||||
options: [
|
||||
{
|
||||
label: 'User Options',
|
||||
group: [
|
||||
{
|
||||
label: 'Suboption 1',
|
||||
value: 'suboption1',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
const tree = renderer.create(reduxWrapper(<EditableSelectField {...propsWithGroup} />)).toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders selectOptions when option does not have a group', () => {
|
||||
const propsWithoutGroup = {
|
||||
...props,
|
||||
options: [
|
||||
{
|
||||
label: 'Default Option',
|
||||
value: 'defaultOption',
|
||||
},
|
||||
],
|
||||
};
|
||||
const tree = renderer.create(reduxWrapper(<EditableSelectField {...propsWithoutGroup} />)).toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders selectOptions with multiple groups', () => {
|
||||
const propsWithGroups = {
|
||||
...props,
|
||||
options: [
|
||||
{
|
||||
label: 'Mixed Options',
|
||||
group: [
|
||||
{
|
||||
label: 'Suboption 1',
|
||||
value: 'suboption1',
|
||||
},
|
||||
{
|
||||
label: 'Suboption 2',
|
||||
value: 'suboption2',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
const tree = renderer.create(reduxWrapper(<EditableSelectField {...propsWithGroups} />)).toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -1,21 +1,16 @@
|
||||
import React from 'react';
|
||||
import renderer from 'react-test-renderer';
|
||||
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import { initializeMockApp, mergeConfig, setConfig } from '@edx/frontend-platform';
|
||||
|
||||
import JumpNav from '../JumpNav';
|
||||
import configureStore from '../../data/configureStore';
|
||||
|
||||
const IntlJumpNav = injectIntl(JumpNav);
|
||||
|
||||
describe('JumpNav', () => {
|
||||
mergeConfig({
|
||||
ENABLE_DEMOGRAPHICS_COLLECTION: false,
|
||||
ENABLE_ACCOUNT_DELETION: true,
|
||||
});
|
||||
|
||||
let props = {};
|
||||
let store;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -28,10 +23,6 @@ describe('JumpNav', () => {
|
||||
},
|
||||
});
|
||||
|
||||
props = {
|
||||
intl: {},
|
||||
displayDemographicsLink: false,
|
||||
};
|
||||
store = configureStore({
|
||||
notificationPreferences: {
|
||||
showPreferences: false,
|
||||
@@ -39,43 +30,35 @@ describe('JumpNav', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should not render Optional Information or delete account link', () => {
|
||||
it('should not render delete account link', async () => {
|
||||
setConfig({
|
||||
ENABLE_ACCOUNT_DELETION: false,
|
||||
});
|
||||
|
||||
const tree = renderer.create((
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<AppProvider store={store}>
|
||||
<IntlJumpNav {...props} />
|
||||
<JumpNav />
|
||||
</AppProvider>
|
||||
</IntlProvider>
|
||||
))
|
||||
.toJSON();
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
expect(tree).toMatchSnapshot();
|
||||
expect(await screen.queryByText('Delete My Account')).toBeNull();
|
||||
});
|
||||
|
||||
it('should render Optional Information and delete account link', () => {
|
||||
it('should render delete account link', async () => {
|
||||
setConfig({
|
||||
ENABLE_DEMOGRAPHICS_COLLECTION: true,
|
||||
ENABLE_ACCOUNT_DELETION: true,
|
||||
});
|
||||
|
||||
props = {
|
||||
...props,
|
||||
displayDemographicsLink: true,
|
||||
};
|
||||
|
||||
const tree = renderer.create((
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<AppProvider store={store}>
|
||||
<IntlJumpNav {...props} />
|
||||
<JumpNav />
|
||||
</AppProvider>
|
||||
</IntlProvider>
|
||||
))
|
||||
.toJSON();
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
expect(tree).toMatchSnapshot();
|
||||
expect(await screen.findByText('Delete My Account')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,483 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`EditableSelectField renders EditableSelectField correctly with editing disabled 1`] = `
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
{
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Main Label
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil mr-1"
|
||||
data-icon="pencil"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={{}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M410.3 231l11.3-11.3-33.9-33.9-62.1-62.1L291.7 89.8l-11.3 11.3-22.6 22.6L58.6 322.9c-10.4 10.4-18 23.3-22.2 37.4L1 480.7c-2.5 8.4-.2 17.5 6.1 23.7s15.3 8.5 23.7 6.1l120.3-35.4c14.1-4.2 27-11.8 37.4-22.2L387.7 253.7 410.3 231zM160 399.4l-9.1 22.7c-4 3.1-8.5 5.4-13.3 6.9L59.4 452l23-78.1c1.4-4.9 3.8-9.4 6.9-13.3l22.7-9.1 0 32c0 8.8 7.2 16 16 16l32 0zM362.7 18.7L348.3 33.2 325.7 55.8 314.3 67.1l33.9 33.9 62.1 62.1 33.9 33.9 11.3-11.3 22.6-22.6 14.5-14.5c25-25 25-65.5 0-90.5L453.3 18.7c-25-25-65.5-25-90.5 0zm-47.4 168l-144 144c-6.2 6.2-16.4 6.2-22.6 0s-6.2-16.4 0-22.6l144-144c6.2-6.2 16.4-6.2 22.6 0s6.2 16.4 0 22.6z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Test Field
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
>
|
||||
Default Confirmation Message
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`EditableSelectField renders EditableSelectField correctly with editing enabled 1`] = `
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
{
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<form
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<div
|
||||
className="pgn__form-group"
|
||||
>
|
||||
<label
|
||||
className="pgn__form-label h6 d-block"
|
||||
htmlFor="field-testField"
|
||||
size="sm"
|
||||
>
|
||||
Main Label
|
||||
</label>
|
||||
<div
|
||||
className="pgn__form-control-decorator-group"
|
||||
>
|
||||
<text
|
||||
aria-describedby="field-testField-2"
|
||||
className="has-value form-control is-invalid"
|
||||
data-hj-suppress={true}
|
||||
id="field-testField"
|
||||
name="testField"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
type="text"
|
||||
value="Test Field"
|
||||
>
|
||||
<option
|
||||
value="defaultOption"
|
||||
>
|
||||
Default Option
|
||||
</option>
|
||||
<optgroup
|
||||
label="User Options"
|
||||
>
|
||||
<option
|
||||
value="suboption1"
|
||||
>
|
||||
Suboption 1
|
||||
</option>
|
||||
</optgroup>
|
||||
<optgroup
|
||||
label="Other Options"
|
||||
>
|
||||
<option
|
||||
value="suboption2"
|
||||
>
|
||||
Suboption 2
|
||||
</option>
|
||||
<option
|
||||
value="suboption3"
|
||||
>
|
||||
Suboption 3
|
||||
</option>
|
||||
</optgroup>
|
||||
</text>
|
||||
</div>
|
||||
<div
|
||||
className="pgn__form-text pgn__form-text-default"
|
||||
>
|
||||
<div>
|
||||
Helpful Text
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn__form-control-description pgn__form-text pgn__form-text-invalid"
|
||||
id="field-testField-2"
|
||||
>
|
||||
<span
|
||||
className="pgn__icon"
|
||||
>
|
||||
<svg
|
||||
aria-hidden={true}
|
||||
fill="none"
|
||||
focusable={false}
|
||||
height={24}
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width={24}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<div />
|
||||
</div>
|
||||
</div>
|
||||
<p>
|
||||
<button
|
||||
aria-disabled={false}
|
||||
aria-live="assertive"
|
||||
className="pgn__stateful-btn pgn__stateful-btn-state-default mr-2 btn btn-primary"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="submit"
|
||||
>
|
||||
<span
|
||||
className="d-flex align-items-center justify-content-center"
|
||||
>
|
||||
<span>
|
||||
Save
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-outline-primary"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`EditableSelectField renders EditableSelectField with an error 1`] = `
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
{
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Main Label
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil mr-1"
|
||||
data-icon="pencil"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={{}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M410.3 231l11.3-11.3-33.9-33.9-62.1-62.1L291.7 89.8l-11.3 11.3-22.6 22.6L58.6 322.9c-10.4 10.4-18 23.3-22.2 37.4L1 480.7c-2.5 8.4-.2 17.5 6.1 23.7s15.3 8.5 23.7 6.1l120.3-35.4c14.1-4.2 27-11.8 37.4-22.2L387.7 253.7 410.3 231zM160 399.4l-9.1 22.7c-4 3.1-8.5 5.4-13.3 6.9L59.4 452l23-78.1c1.4-4.9 3.8-9.4 6.9-13.3l22.7-9.1 0 32c0 8.8 7.2 16 16 16l32 0zM362.7 18.7L348.3 33.2 325.7 55.8 314.3 67.1l33.9 33.9 62.1 62.1 33.9 33.9 11.3-11.3 22.6-22.6 14.5-14.5c25-25 25-65.5 0-90.5L453.3 18.7c-25-25-65.5-25-90.5 0zm-47.4 168l-144 144c-6.2 6.2-16.4 6.2-22.6 0s-6.2-16.4 0-22.6l144-144c6.2-6.2 16.4-6.2 22.6 0s6.2 16.4 0 22.6z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Test Field
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
>
|
||||
Default Confirmation Message
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`EditableSelectField renders selectOptions when option does not have a group 1`] = `
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
{
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Main Label
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil mr-1"
|
||||
data-icon="pencil"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={{}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M410.3 231l11.3-11.3-33.9-33.9-62.1-62.1L291.7 89.8l-11.3 11.3-22.6 22.6L58.6 322.9c-10.4 10.4-18 23.3-22.2 37.4L1 480.7c-2.5 8.4-.2 17.5 6.1 23.7s15.3 8.5 23.7 6.1l120.3-35.4c14.1-4.2 27-11.8 37.4-22.2L387.7 253.7 410.3 231zM160 399.4l-9.1 22.7c-4 3.1-8.5 5.4-13.3 6.9L59.4 452l23-78.1c1.4-4.9 3.8-9.4 6.9-13.3l22.7-9.1 0 32c0 8.8 7.2 16 16 16l32 0zM362.7 18.7L348.3 33.2 325.7 55.8 314.3 67.1l33.9 33.9 62.1 62.1 33.9 33.9 11.3-11.3 22.6-22.6 14.5-14.5c25-25 25-65.5 0-90.5L453.3 18.7c-25-25-65.5-25-90.5 0zm-47.4 168l-144 144c-6.2 6.2-16.4 6.2-22.6 0s-6.2-16.4 0-22.6l144-144c6.2-6.2 16.4-6.2 22.6 0s6.2 16.4 0 22.6z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Test Field
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
>
|
||||
Default Confirmation Message
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`EditableSelectField renders selectOptions when option has a group 1`] = `
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
{
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Main Label
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil mr-1"
|
||||
data-icon="pencil"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={{}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M410.3 231l11.3-11.3-33.9-33.9-62.1-62.1L291.7 89.8l-11.3 11.3-22.6 22.6L58.6 322.9c-10.4 10.4-18 23.3-22.2 37.4L1 480.7c-2.5 8.4-.2 17.5 6.1 23.7s15.3 8.5 23.7 6.1l120.3-35.4c14.1-4.2 27-11.8 37.4-22.2L387.7 253.7 410.3 231zM160 399.4l-9.1 22.7c-4 3.1-8.5 5.4-13.3 6.9L59.4 452l23-78.1c1.4-4.9 3.8-9.4 6.9-13.3l22.7-9.1 0 32c0 8.8 7.2 16 16 16l32 0zM362.7 18.7L348.3 33.2 325.7 55.8 314.3 67.1l33.9 33.9 62.1 62.1 33.9 33.9 11.3-11.3 22.6-22.6 14.5-14.5c25-25 25-65.5 0-90.5L453.3 18.7c-25-25-65.5-25-90.5 0zm-47.4 168l-144 144c-6.2 6.2-16.4 6.2-22.6 0s-6.2-16.4 0-22.6l144-144c6.2-6.2 16.4-6.2 22.6 0s6.2 16.4 0 22.6z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Test Field
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
>
|
||||
Default Confirmation Message
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`EditableSelectField renders selectOptions with multiple groups 1`] = `
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
{
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Main Label
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil mr-1"
|
||||
data-icon="pencil"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={{}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M410.3 231l11.3-11.3-33.9-33.9-62.1-62.1L291.7 89.8l-11.3 11.3-22.6 22.6L58.6 322.9c-10.4 10.4-18 23.3-22.2 37.4L1 480.7c-2.5 8.4-.2 17.5 6.1 23.7s15.3 8.5 23.7 6.1l120.3-35.4c14.1-4.2 27-11.8 37.4-22.2L387.7 253.7 410.3 231zM160 399.4l-9.1 22.7c-4 3.1-8.5 5.4-13.3 6.9L59.4 452l23-78.1c1.4-4.9 3.8-9.4 6.9-13.3l22.7-9.1 0 32c0 8.8 7.2 16 16 16l32 0zM362.7 18.7L348.3 33.2 325.7 55.8 314.3 67.1l33.9 33.9 62.1 62.1 33.9 33.9 11.3-11.3 22.6-22.6 14.5-14.5c25-25 25-65.5 0-90.5L453.3 18.7c-25-25-65.5-25-90.5 0zm-47.4 168l-144 144c-6.2 6.2-16.4 6.2-22.6 0s-6.2-16.4 0-22.6l144-144c6.2-6.2 16.4-6.2 22.6 0s6.2 16.4 0 22.6z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Test Field
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
>
|
||||
Default Confirmation Message
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,181 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`JumpNav should not render Optional Information or delete account link 1`] = `
|
||||
<div
|
||||
className="jump-nav px-2.25 jump-nav-sm position-sticky pt-3"
|
||||
>
|
||||
<ul
|
||||
className="list-unstyled"
|
||||
style={Object {}}
|
||||
>
|
||||
<li
|
||||
className=""
|
||||
>
|
||||
<a
|
||||
aria-current="page"
|
||||
className="active"
|
||||
href="/#basic-information"
|
||||
isActive={[Function]}
|
||||
onClick={[Function]}
|
||||
>
|
||||
Account Information
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
className=""
|
||||
>
|
||||
<a
|
||||
aria-current="page"
|
||||
className="active"
|
||||
href="/#profile-information"
|
||||
isActive={[Function]}
|
||||
onClick={[Function]}
|
||||
>
|
||||
Profile Information
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
className=""
|
||||
>
|
||||
<a
|
||||
aria-current="page"
|
||||
className="active"
|
||||
href="/#social-media"
|
||||
isActive={[Function]}
|
||||
onClick={[Function]}
|
||||
>
|
||||
Social Media Links
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
className=""
|
||||
>
|
||||
<a
|
||||
aria-current="page"
|
||||
className="active"
|
||||
href="/#site-preferences"
|
||||
isActive={[Function]}
|
||||
onClick={[Function]}
|
||||
>
|
||||
Site Preferences
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
className=""
|
||||
>
|
||||
<a
|
||||
aria-current="page"
|
||||
className="active"
|
||||
href="/#linked-accounts"
|
||||
isActive={[Function]}
|
||||
onClick={[Function]}
|
||||
>
|
||||
Linked Accounts
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`JumpNav should render Optional Information and delete account link 1`] = `
|
||||
<div
|
||||
className="jump-nav px-2.25 jump-nav-sm position-sticky pt-3"
|
||||
>
|
||||
<ul
|
||||
className="list-unstyled"
|
||||
style={Object {}}
|
||||
>
|
||||
<li
|
||||
className=""
|
||||
>
|
||||
<a
|
||||
aria-current="page"
|
||||
className="active"
|
||||
href="/#basic-information"
|
||||
isActive={[Function]}
|
||||
onClick={[Function]}
|
||||
>
|
||||
Account Information
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
className=""
|
||||
>
|
||||
<a
|
||||
aria-current="page"
|
||||
className="active"
|
||||
href="/#profile-information"
|
||||
isActive={[Function]}
|
||||
onClick={[Function]}
|
||||
>
|
||||
Profile Information
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
className=""
|
||||
>
|
||||
<a
|
||||
aria-current="page"
|
||||
className="active"
|
||||
href="/#demographics-information"
|
||||
isActive={[Function]}
|
||||
onClick={[Function]}
|
||||
>
|
||||
Optional Information
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
className=""
|
||||
>
|
||||
<a
|
||||
aria-current="page"
|
||||
className="active"
|
||||
href="/#social-media"
|
||||
isActive={[Function]}
|
||||
onClick={[Function]}
|
||||
>
|
||||
Social Media Links
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
className=""
|
||||
>
|
||||
<a
|
||||
aria-current="page"
|
||||
className="active"
|
||||
href="/#site-preferences"
|
||||
isActive={[Function]}
|
||||
onClick={[Function]}
|
||||
>
|
||||
Site Preferences
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
className=""
|
||||
>
|
||||
<a
|
||||
aria-current="page"
|
||||
className="active"
|
||||
href="/#linked-accounts"
|
||||
isActive={[Function]}
|
||||
onClick={[Function]}
|
||||
>
|
||||
Linked Accounts
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
className=""
|
||||
>
|
||||
<a
|
||||
aria-current="page"
|
||||
className="active"
|
||||
href="/#delete-account"
|
||||
isActive={[Function]}
|
||||
onClick={[Function]}
|
||||
>
|
||||
Delete My Account
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
119
src/account-settings/test/mockData.js
Normal file
119
src/account-settings/test/mockData.js
Normal file
@@ -0,0 +1,119 @@
|
||||
const mockData = {
|
||||
accountSettings: {
|
||||
loading: false,
|
||||
loaded: true,
|
||||
loadingError: null,
|
||||
data: null,
|
||||
values: {
|
||||
username: 'test_username',
|
||||
country: 'US',
|
||||
accomplishments_shared: false,
|
||||
name: 'test_name',
|
||||
email: 'test_email@test.com',
|
||||
id: 533,
|
||||
verified_name: null,
|
||||
extended_profile: [
|
||||
{
|
||||
field_name: 'work_experience',
|
||||
field_value: '',
|
||||
},
|
||||
],
|
||||
gender: 'm',
|
||||
'pref-lang': 'en',
|
||||
level_of_education: 'b',
|
||||
language_proficiencies: 'es',
|
||||
social_link_linkedin: 'https://linkedin.com/in/testuser',
|
||||
social_link_facebook: '',
|
||||
social_link_twitter: '',
|
||||
time_zone: 'America/New_York',
|
||||
state: 'NY',
|
||||
secondary_email_enabled: true,
|
||||
secondary_email: 'test_recovery@test.com',
|
||||
year_of_birth: '1990',
|
||||
},
|
||||
errors: {},
|
||||
confirmationValues: {},
|
||||
drafts: {},
|
||||
saveState: null,
|
||||
timeZones: [
|
||||
{
|
||||
time_zone: 'America/New_York',
|
||||
description: 'America/New_York (EST, UTC-0500)',
|
||||
},
|
||||
],
|
||||
countryTimeZones: [
|
||||
{
|
||||
time_zone: 'America/New_York',
|
||||
description: 'America/New_York (EST, UTC-0500)',
|
||||
},
|
||||
],
|
||||
previousSiteLanguage: null,
|
||||
deleteAccount: {
|
||||
status: null,
|
||||
errorType: null,
|
||||
},
|
||||
siteLanguage: {
|
||||
loading: false,
|
||||
loaded: true,
|
||||
loadingError: null,
|
||||
siteLanguageList: [
|
||||
{
|
||||
code: 'en',
|
||||
name: 'English',
|
||||
released: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
resetPassword: {
|
||||
status: null,
|
||||
},
|
||||
nameChange: {
|
||||
saveState: null,
|
||||
errors: {},
|
||||
},
|
||||
thirdPartyAuth: {
|
||||
providers: [
|
||||
{
|
||||
id: 'oa2-google-oauth2',
|
||||
name: 'Google',
|
||||
connected: false,
|
||||
accepts_logins: true,
|
||||
connectUrl: 'http://localhost:18000/auth/login/google-oauth2/?auth_entry=account_settings&next=%2Faccount%2Fsettings',
|
||||
disconnectUrl: 'http://localhost:18000/auth/disconnect/google-oauth2/?',
|
||||
},
|
||||
],
|
||||
disconnectionStatuses: {},
|
||||
errors: {},
|
||||
},
|
||||
verifiedName: null,
|
||||
mostRecentVerifiedName: {},
|
||||
verifiedNameHistory: {
|
||||
use_verified_name_for_certs: false,
|
||||
results: [],
|
||||
},
|
||||
profileDataManager: null,
|
||||
},
|
||||
notificationPreferences: {
|
||||
showPreferences: true,
|
||||
courses: {
|
||||
status: 'success',
|
||||
courses: [],
|
||||
pagination: {
|
||||
count: 0,
|
||||
currentPage: 1,
|
||||
hasMore: false,
|
||||
totalPages: 1,
|
||||
},
|
||||
},
|
||||
preferences: {
|
||||
status: 'idle',
|
||||
updatePreferenceStatus: 'idle',
|
||||
selectedCourse: 'account',
|
||||
preferences: [],
|
||||
apps: [],
|
||||
nonEditable: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default mockData;
|
||||
@@ -2,7 +2,7 @@ import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink, StatefulButton } from '@edx/paragon';
|
||||
import { Hyperlink, StatefulButton } from '@openedx/paragon';
|
||||
|
||||
import Alert from '../Alert';
|
||||
import { disconnectAuth } from './data/actions';
|
||||
|
||||
@@ -16,8 +16,9 @@ export async function getThirdPartyAuthProviders() {
|
||||
}
|
||||
|
||||
export async function postDisconnectAuth(url) {
|
||||
const requestConfig = { headers: { Accept: 'application/json' } };
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.post(url)
|
||||
.post(url, {}, requestConfig)
|
||||
.catch(handleRequestError);
|
||||
return data;
|
||||
}
|
||||
|
||||
16
src/divider/Divider.jsx
Normal file
16
src/divider/Divider.jsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
|
||||
const Divider = ({ className, ...props }) => (
|
||||
<div className={classNames('divider', className)} {...props} />
|
||||
);
|
||||
|
||||
Divider.propTypes = {
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
Divider.defaultProps = {
|
||||
className: undefined,
|
||||
};
|
||||
|
||||
export default Divider;
|
||||
2
src/divider/index.jsx
Normal file
2
src/divider/index.jsx
Normal file
@@ -0,0 +1,2 @@
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export { default as Divider } from './Divider';
|
||||
@@ -1,21 +1,19 @@
|
||||
import React from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const Head = ({ intl }) => (
|
||||
<Helmet>
|
||||
<title>
|
||||
{intl.formatMessage(messages['account.page.title'], { siteName: getConfig().SITE_NAME })}
|
||||
</title>
|
||||
<link rel="shortcut icon" href={getConfig().FAVICON_URL} type="image/x-icon" />
|
||||
</Helmet>
|
||||
);
|
||||
|
||||
Head.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
const Head = () => {
|
||||
const intl = useIntl();
|
||||
return (
|
||||
<Helmet>
|
||||
<title>
|
||||
{intl.formatMessage(messages['account.page.title'], { siteName: getConfig().SITE_NAME })}
|
||||
</title>
|
||||
<link rel="shortcut icon" href={getConfig().FAVICON_URL} type="image/x-icon" />
|
||||
</Helmet>
|
||||
);
|
||||
};
|
||||
|
||||
export default injectIntl(Head);
|
||||
export default Head;
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import React from 'react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { mount } from 'enzyme';
|
||||
import { render } from '@testing-library/react';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import Head from './Head';
|
||||
|
||||
describe('Head', () => {
|
||||
const props = {};
|
||||
it('should match render title tag and fivicon with the site configuration values', () => {
|
||||
mount(<IntlProvider locale="en"><Head {...props} /></IntlProvider>);
|
||||
render(<IntlProvider locale="en"><Head /></IntlProvider>);
|
||||
const helmet = Helmet.peek();
|
||||
expect(helmet.title).toEqual(`Account | ${getConfig().SITE_NAME}`);
|
||||
expect(helmet.linkTags[0].rel).toEqual('shortcut icon');
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { logError } from '@edx/frontend-platform/logging';
|
||||
import { breakpoints, useWindowSize } from '@openedx/paragon';
|
||||
|
||||
import {
|
||||
IDLE_STATUS, LOADING_STATUS, SUCCESS_STATUS, FAILURE_STATUS,
|
||||
@@ -66,3 +68,8 @@ export function useFeedbackWrapper() {
|
||||
}
|
||||
}, []);
|
||||
}
|
||||
|
||||
export function useIsOnMobile() {
|
||||
const windowSize = useWindowSize();
|
||||
return windowSize.width <= breakpoints.small.minWidth;
|
||||
}
|
||||
|
||||
@@ -1,44 +1 @@
|
||||
import { messages as headerMessages } from '@edx/frontend-component-header';
|
||||
import { messages as footerMessages } from '@edx/frontend-component-footer';
|
||||
import { messages as paragonMessages } from '@edx/paragon';
|
||||
import arMessages from './messages/ar.json';
|
||||
import deMessages from './messages/de.json';
|
||||
import es419Messages from './messages/es_419.json';
|
||||
import faIRMessages from './messages/fa_IR.json';
|
||||
import frMessages from './messages/fr.json';
|
||||
import frCAMessages from './messages/fr_CA.json';
|
||||
import hiMessages from './messages/hi.json';
|
||||
import itMessages from './messages/it.json';
|
||||
import ptMessages from './messages/pt.json';
|
||||
import ruMessages from './messages/ru.json';
|
||||
import ukMessages from './messages/uk.json';
|
||||
import zhcnMessages from './messages/zh_CN.json';
|
||||
import dedeCAMessages from './messages/de_DE.json';
|
||||
import ititCAMessages from './messages/it_IT.json';
|
||||
import ptptCAMessages from './messages/pt_PT.json';
|
||||
// no need to import en messages-- they are in the defaultMessage field
|
||||
|
||||
const appMessages = {
|
||||
ar: arMessages,
|
||||
'es-419': es419Messages,
|
||||
'fa-ir': faIRMessages,
|
||||
fr: frMessages,
|
||||
'zh-cn': zhcnMessages,
|
||||
pt: ptMessages,
|
||||
it: itMessages,
|
||||
de: deMessages,
|
||||
hi: hiMessages,
|
||||
'fr-ca': frCAMessages,
|
||||
ru: ruMessages,
|
||||
uk: ukMessages,
|
||||
'de-de': dedeCAMessages,
|
||||
'it-it': ititCAMessages,
|
||||
'pt-pt': ptptCAMessages,
|
||||
};
|
||||
|
||||
export default [
|
||||
headerMessages,
|
||||
paragonMessages,
|
||||
footerMessages,
|
||||
appMessages,
|
||||
];
|
||||
export default [];
|
||||
|
||||
@@ -1,370 +0,0 @@
|
||||
{
|
||||
"account.settings.message.duplicate.tpa.provider": "حساب {provider} الذي حددته موصول من قبل بحساب آخر على {siteName}.",
|
||||
"account.settings.message.managed.settings": "إعدادات ملفك الشخصي يديرها {ManagerTitle}. اتصل بمديرك أو ب{support} للحصول على المساعدة.",
|
||||
"account.settings.message.managed.settings.support": "الدعم",
|
||||
"account.settings.page.heading": "إعدادات الحساب",
|
||||
"account.settings.loading.message": "التحميل جارٍ...",
|
||||
"account.settings.loading.error": "خطأ: {error}",
|
||||
"account.settings.banner.beta.language": "لقد قمت بضبط لغتك على {beta_language}، وهي حاليا غير مترجمة بالكامل. يمكنك مساعدتنا في إتمام ترجمة هذه اللغة من خلال الانضمام إلى مجتمع Transifex وإضافة ترجمات من الإنجليزية للمتعلمين الذين يتحدثون {beta_language}.",
|
||||
"account.settings.banner.beta.language.action.switch.back": "العودة إلى {previous_language}",
|
||||
"account.settings.banner.beta.language.action.help.translate": "ساعد في الترجمة إلى {beta_language}",
|
||||
"account.settings.section.account.information": "معلومات الحساب",
|
||||
"account.settings.section.account.information.description": "تتضمن هذه الإعدادات معلومات أساسية عن حسابك.",
|
||||
"account.settings.section.profile.information": "معلومات الملف الشخصي",
|
||||
"account.settings.section.demographics.information": "معلومات اختيارية",
|
||||
"account.settings.section.site.preferences": "تفضيلات الموقع",
|
||||
"account.settings.section.linked.accounts": "الحسابات الموصولة",
|
||||
"account.settings.section.linked.accounts.description": "يمكنك وصل حسابات هويتك لتبسيط تسجيل دخولك إلى {siteName}.",
|
||||
"account.settings.field.username": "اسم المستخدم",
|
||||
"account.settings.field.username.help.text": "الاسم الذي يعرّفك على {siteName}. لا يمكنك تغيير اسم المستخدم الخاص بك.",
|
||||
"account.settings.field.full.name": "الاسم الكامل",
|
||||
"account.settings.field.full.name.empty": "إضافة الاسم",
|
||||
"account.settings.field.full.name.help.text": "الاسم المستعمل للتحقق من هويتك والذي يظهر في شهاداتك.",
|
||||
"account.settings.field.full.name.help.text.default": "الاسم الذي يظهر في ملفك الشخصي العامّ.",
|
||||
"account.settings.field.full.name.help.text.default.certificate": "سيظهر هذا الاسم في شهاداتك و سجلّاتك العامّة.",
|
||||
"account.settings.field.name.verified": "اسم متحقَّق منه",
|
||||
"account.settings.field.name.verified.help.text.verified": "تم التحقق من هذا الاسم عن طريق بطاقة تعريف ذات صورة.",
|
||||
"account.settings.field.name.verified.help.text.verified.proctored": "تم التحقق من هذا الاسم عن طريق المراقبة.",
|
||||
"account.settings.field.name.verified.help.text.verified.certificate": "تم التحقق من هذا الاسم عن طريق بطاقة هوية ذات صورة، وتم اختياره للظهور في الشهادات والسجلات العامة.",
|
||||
"account.settings.field.name.verified.help.text.verified.proctored.certificate": "تم التحقق من هذا الاسم من خلال المراقبة، وتم تحديده للظهور في الشهادات والسجلات العامة.",
|
||||
"account.settings.field.name.verified.help.text.submitted": "تم تسليم طلب التحقق. في العادة يستغرق هذا 48 ساعة أو أقل. لا يمكن تغيير الاسم المتحقَّق منه في هذا الوقت.",
|
||||
"account.settings.field.name.verified.help.text.submitted.proctored": "تم تسليم امتحانك المراقَب. لا يمكن تغيير الاسم المتحقَّق منه في هذا الوقت. رجاءً أعد التفقد بعد 2-5 أيام.",
|
||||
"account.settings.field.name.verified.help.text.submitted.certificate": "عند نجاح التحقق من الهوية، سيظهر هذا الاسم على شهاداتك و سجلاتك العامة. لا يمكن تغيير الاسم المتحقَّق منه في هذا الوقت.",
|
||||
"account.settings.field.name.verified.help.text.submitted.proctored.certificate": "بمجرد اجتياز امتحانك المراقب للمراجعة، سيظهر هذا الاسم في شهادتك و سجلاتك العامة. لا يمكن تغيير الاسم المتحقَّق منه في هذا الوقت.",
|
||||
"account.settings.field.name.verified.verification.help": "أدخل اسمك كما يظهر في بطاقة تعريف الطالب أو الموظف الخاصة بك، أو بطاقة تعريفك الصادرة عن الحكومة.",
|
||||
"account.settings.field.full.name.help.text.submitted": "تم تسليم طلب التحقق. يستغرق هذا عادةً 48 ساعة أو أقل. لا يمكن تغيير الاسم الكامل في هذا الوقت.",
|
||||
"account.settings.field.full.name.help.text.submitted.proctored": "تم تسليم امتحانك المراقب. لا يمكن تغيير الاسم الكامل في هذا الوقت. يرجى التحقق مرة أخرى خلال 2-5 أيام.",
|
||||
"account.settings.field.full.name.help.text.submitted.certificate": "عند نجاح التحقق من الهوية، سيظهر هذا الاسم على الشهادات والسجلات العامة. لا يمكن تغيير الاسم الكامل في هذا الوقت.",
|
||||
"account.settings.field.full.name.help.text.submitted.proctored.certificate": "بمجرد اجتياز امتحانك المراقب للمراجعة، سيظهر هذا الاسم في شهاداتك وسجلاتك العامة. لا يمكن تغيير الاسم الكامل في هذا الوقت.",
|
||||
"account.settings.field.name.verified.success.message": "اكتمل طلب التحقق من هويتك بنجاح. لديك الآن خيار تحديد الاسم الذي تفضل ظهوره على شهاداتك وسجلاتك العامة.",
|
||||
"account.settings.field.name.verified.success.message.header": "اكتمل طلب تغيير اسمك!",
|
||||
"account.settings.field.name.verified.failure.message": "لم تنجح آخر محاولاتك للتحقق من هويتك. تم إرجاع إعدادات الحساب ذات الصلة إلى وضعها السابق.",
|
||||
"account.settings.field.name.verified.failure.message.header": "لم نستطع التحقق من هويتك.",
|
||||
"account.settings.field.name.verified.failure.message.help.link": "اعرف المزيد عن التحقق من الهوية.",
|
||||
"account.settings.field.name.verified.submitted.message": "تم تسليم طلب التحقق من هويتك و عادة ما يستغرق إكماله ما بين 24 و 48 ساعة.",
|
||||
"account.settings.field.name.verified.submitted.message.certificate": "عند الموافقة على طلبك، سيظهر اسمك المحدَّث على جميع الشهادات السجلات العامة ذات الصلة.",
|
||||
"account.settings.field.name.verified.submitted.message.header": "طلب تغيير اسمك يكاد يكتمل!",
|
||||
"account.settings.field.email": "البريد الالكتروني (تسجيل الدخول)",
|
||||
"account.settings.field.email.empty": "إضافة عنوان البريد الإلكتروني",
|
||||
"account.settings.field.email.confirmation": "لقد أرسلنا رسالة تأكيد إلى {value}. انقر على الرابط في الرسالة لتحديث عنوان بريدك الإلكتروني.",
|
||||
"account.settings.field.email.help.text": "على هذا العنوان تصلك الرسائل من {siteName} و من فرق المساقات.",
|
||||
"account.settings.field.secondary.email": "عنوان بريد الاستعادة الإلكتروني.",
|
||||
"account.settings.field.secondary.email.empty": "إضافة عنوان بريد إلكتروني لاستعادة حسابك",
|
||||
"account.settings.field.secondary.email.confirmation": "لقد أرسلنا رسالة تأكيد إلى {value}. انقر على الرابط في الرسالة لتحديث عنوان بريد الاستعادة الإلكتروني الخاص بك.",
|
||||
"account.settings.email.field.confirmation.header": "تعليق عملية التأكيد",
|
||||
"account.settings.field.dob": "سنة الميلاد",
|
||||
"account.settings.field.dob.empty": "إضافة سنة الميلاد",
|
||||
"account.settings.field.year_of_birth.options.empty": "تحديد سنة الميلاد",
|
||||
"account.settings.field.dob.month": "الشهر",
|
||||
"account.settings.field.dob.year": "السنة",
|
||||
"account.settings.field.month.year.default": "اختر الشهر",
|
||||
"account.settings.field.dob.year.default": "اختر السنة",
|
||||
"account.settings.field.dob.form.button": "يرجى تأكيد تاريخ ميلادك",
|
||||
"account.settings.field.dob.form.title": "أدخل شهر وسنة ميلادك",
|
||||
"account.settings.field.dob.form.help.text": "نطلب معلومات شهر وسنة الميلاد لمساعدتنا على الامتثال لالتزاماتنا القانونية.",
|
||||
"account.settings.field.dob.form.success": "شكرا لك على إدخال المعلومات الخاصة بك.",
|
||||
"account.settings.field.month_of_birth.options.empty": "تحديد شهر الميلاد",
|
||||
"account.settingsfield.dob.error.general": "حدث خطأ تقني. رجاءً حاول مجددا.",
|
||||
"account.settings.field.country": "البلد",
|
||||
"account.settings.field.country.empty": "إضافة البلد ",
|
||||
"account.settings.field.country.options.empty": "اختر البلد",
|
||||
"account.settings.field.state": "المنطقة / الولاية / المحافظة",
|
||||
"account.settings.field.state.empty": "إضافة المنطقة / الولاية / المحافظة",
|
||||
"account.settings.field.state.options.empty": "حدد المنطقة",
|
||||
"account.settings.field.site.language": "لغة الموقع",
|
||||
"account.settings.field.site.language.help.text": "اللغة المستخدمة في كافة أقسام هذا الموقع. يتوفّر هذا الموقع حاليًا بعدد محدود من اللغات.",
|
||||
"account.settings.field.education": "المستوى التعليمي",
|
||||
"account.settings.field.education.empty": "إضافة المستوى التعليمي",
|
||||
"account.settings.field.education.levels.empty": "اختر مستوى تعليميًّا",
|
||||
"account.settings.field.education.levels.p": "دكتوراه",
|
||||
"account.settings.field.education.levels.m": "ماجستير / ماستر أو شهادة مهنيّة",
|
||||
"account.settings.field.education.levels.b": "بكالوريوس / ليسانس",
|
||||
"account.settings.field.education.levels.a": "درجة الزمالة / دبلوم الدراسات الجامعية",
|
||||
"account.settings.field.education.levels.hs": "الثانوية العامة / البكالوريا",
|
||||
"account.settings.field.education.levels.jhs": "المدرسة الإعدادية / المتوسطة",
|
||||
"account.settings.field.education.levels.el": "المدرسة الابتدائية / الأساسية",
|
||||
"account.settings.field.education.levels.none": "دون تعليم رسمي",
|
||||
"account.settings.field.education.levels.o": "نوع آخر من التعليم",
|
||||
"account.settings.field.gender": "الجنس",
|
||||
"account.settings.field.gender.empty": "إضافة الجنس",
|
||||
"account.settings.field.gender.options.empty": "اختر جنسًا",
|
||||
"account.settings.field.gender.options.f": "أنثى",
|
||||
"account.settings.field.gender.options.m": "ذكر",
|
||||
"account.settings.field.gender.options.o": "آخر",
|
||||
"account.settings.field.language.proficiencies": "لغة التحدّث",
|
||||
"account.settings.field.language.proficiencies.empty": "إضافة لغة التحدث",
|
||||
"account.settings.field.language_proficiencies.options.empty": "اختر لغة",
|
||||
"account.settings.field.time.zone": "المنطقة الزمنية",
|
||||
"account.settings.field.time.zone.empty": "ضبط المنطقة الزمنية",
|
||||
"account.settings.field.time.zone.description": "حدد المنطقة الزمنية لعرض تواريخ المساقات. إذا لم تحدد منطقة زمنية، فسيتم عرض تواريخ المساق، بما في ذلك المواعيد النهائية للواجبات بالتوقيت المحلّي للمتصفح.",
|
||||
"account.settings.field.time.zone.default": "الافتراضي (المنطقة الزمنية المحلية)",
|
||||
"account.settings.field.time.zone.all": "جميع المناطق الزمنية",
|
||||
"account.settings.field.time.zone.country": "المناطق الزمنية للبلدان",
|
||||
"account.settings.section.social.media": "روابط التواصل الإجتماعي",
|
||||
"account.settings.section.social.media.description": "اختياريًا، اربط حساباتك الشخصية بأيقونات التواصل الاجتماعي في ملفك على {siteName}.",
|
||||
"account.settings.field.social.platform.name.linkedin": "لينكد إن",
|
||||
"account.settings.field.social.platform.name.linkedin.empty": "إضافة سيرة لينكد إن",
|
||||
"account.settings.jump.nav.delete.account": "حذف حسابي",
|
||||
"account.settings.field.social.platform.name.twitter": "تويتر",
|
||||
"account.settings.field.social.platform.name.twitter.empty": "إضافة صفحة تويتر",
|
||||
"account.settings.field.social.platform.name.facebook": "فيسبوك",
|
||||
"account.settings.field.social.platform.name.facebook.empty": "إضافة حساب فيسبوك",
|
||||
"account.settings.editable.field.action.save": "حفظ",
|
||||
"account.settings.editable.field.action.cancel": "إلغاء",
|
||||
"account.settings.editable.field.action.edit": "تعديل",
|
||||
"account.settings.static.field.empty": "لا قيمة محددة. رجاءً اتصل بمديرك في {enterprise} ليقوم بالتعديلات.",
|
||||
"account.settings.static.field.empty.no.admin": "لا قيمة محددة.",
|
||||
"notification.preferences.notifications.label": "Notifications",
|
||||
"account.settings.field.name.certificate.select": "في حال التأشير، سيظهر هذا الاسم في شهاداتك و سجلاتك العامة.",
|
||||
"account.settings.field.name.modal.certificate.title": "اختر اسمًا مفضلًا للشهادات والسجلات العامة",
|
||||
"account.settings.field.name.modal.certificate.select": "اختر اسمًا",
|
||||
"account.settings.field.name.modal.certificate.option.full": "الاسم الكامل",
|
||||
"account.settings.field.name.modal.certificate.option.verified": "اسم متحقَّق منه",
|
||||
"account.settings.field.name.modal.certificate.button.choose": "اختيار الاسم",
|
||||
"account.settings.coaching.consent.welcome.header": "لنبدأ.",
|
||||
"account.settings.coaching.consent.welcome.subheader": "نحن هنا لأجلك من البداية حتى النهاية",
|
||||
"account.settings.coaching.consent.description": "تتضمن برامج MicroBachelors مرافقة تركز على مهنتك و تعليمك و كيفية تحقيقك للنتائج، و ذلك من خلال التواصل الفردي مع خبير متمرس. إن كنت مهتمًا، فيرجى تزويدنا بالمعلومات أدناه و النقر على \"إرسال\"، و سيتصل بك شريكنا في المرافقة عبر البريد الإلكتروني و/أو الرسائل النصية لمساعدتك على المضي قدمًا. تنطبق الشروط والأحكام.*",
|
||||
"account.settings.coaching.consent.text-messaging.disclaimer": "* خدمات المرافقة مشمولة دون تكاليف إضافية للمتعلمين الذين لديهم أرقام هواتف أمريكية. تتضمن المرافقة رسائل نصية دورية. قد تنطبق أسعار على الرسائل والبيانات. أرسل STOP للانسحاب.",
|
||||
"account.settings.coaching.consent.accept-coaching": "سجّل للاستفادة من خدمات المرافقة",
|
||||
"account.settings.coaching.consent.decline-coaching": "أفضّل ألا يٌتصَل بي بخصوص خدمات المرافقة المجانية",
|
||||
"account.settings.coaching.consent.label.name": "رجاءً أكّد اسمك",
|
||||
"account.settings.coaching.consent.label.phone-number": "أدخل رقم هاتفك الجوّال",
|
||||
"account.settings.coaching.consent.success.header": "نجحت العملية!",
|
||||
"account.settings.coaching.consent.success.message": "أنت الآن مشترك في المرافقة. ترقّب رسالة عبر البريد الإلكتروني أو خدمة الرسائل القصيرة في في الأيام المقبلة.",
|
||||
"account.settings.coaching.consent.success.continue": "البدء في مساقي",
|
||||
"account.settings.coaching.managed.support": "الدعم",
|
||||
"account.settings.coaching.managed.alert": "اسمك يديره {ManagerTitle}. اتصل بمديرك للحصول على المساعدة.",
|
||||
"account.settings.field.phone_number": "رقم الهاتف",
|
||||
"account.settings.field.phone_number.empty": "إضافة رقم هاتف",
|
||||
"account.settings.field.coaching_consent": "الموافقة على المرافقة",
|
||||
"account.settings.field.coaching_consent.tooltip": "تتضمن برامج MicroBachelors مرافقة قائمة على الرسائل النصية، تساعدك على إقران تجاربك التعلّمية مع أهدافك المهنية من خلال النصائح الفردية. خدمات المرافقة مشمولة دون تكاليف إضافية، وهي متوفرة للمتعلمين الذين لديهم أرقام هواتف جوالة أمريكية. تنطبق أسعار المراسلة القياسية. أرسل 'STOP' في أي وقت للانسحاب من الرسائل.",
|
||||
"account.settings.field.coaching_consent.error": "مطلوب رقم هاتف أمريكي صحيح للتسجيل في المرافقة",
|
||||
"account.settings.delete.account.before.proceeding": "قبل المتابعة، يرجى {actionLink}.",
|
||||
"account.settings.delete.account.header": "حذف حسابي",
|
||||
"account.settings.delete.account.subheader": "نأسف لذهابك!",
|
||||
"account.settings.delete.account.text.1": "ترجى الملاحظة: إن حذف حسابك وبياناتك الشخصية له أثر دائم و لا يمكن التراجع عنه. لن يكون بمقدور {siteName} استعادة حسابك ولا البيانات التي يتم حذفها.",
|
||||
"account.settings.delete.account.text.2": "بمجرد حذف حسابك، فإنك لن تستطيع استخدامه لمتابعة المساقات على {siteName}.",
|
||||
"account.settings.delete.account.text.2.edX": "بمجرد حذف حسابك، فإنك لن تستطيع استخدامه لمتابعة المساقات على تطبيق edX ولا edx.org ولا أي موقع آخر تستضيفه edX. وهذا يشمل الوصول إلى edx.org من نظام صاحب العمل أو الجامعة و الوصول إلى المواقع الخاصة التي تقدمها MIT Open Learning و Wharton Executive Education و Harvard Medical School.",
|
||||
"account.settings.delete.account.text.3.link": "اتّبع هذه التعليمات لطباعة أو تحميل شهادة",
|
||||
"account.settings.delete.account.text.warning": "تحذير: حذف الحساب أثره دائم. يرجى قراءة ما ورد أعلاه بعناية قبل المتابعة. هذا إجراء غير رجعي، و لن تتمكن بعده من استخدام نفس البريد الإلكتروني على {siteName}.",
|
||||
"account.settings.delete.account.text.change.instead": "هل تريد بدلاً من ذلك تغيير بريدك الإلكتروني أو اسمك أو كلمة المرور الخاصة بك؟",
|
||||
"account.settings.delete.account.button": "حذف حسابي",
|
||||
"account.settings.delete.account.please.activate": "تفعيل حسابك",
|
||||
"account.settings.delete.account.please.confirm": "تأكيد حسابك",
|
||||
"account.settings.delete.account.please.unlink": "فصل جميع حسابات التواصل الاجتماعي",
|
||||
"account.settings.delete.account.modal.header": "هل أنت متأكد؟",
|
||||
"account.settings.delete.account.modal.text.1": "لقد اخترت \"حذف حسابي\". إن حذف حسابك وبياناتك الشخصية ذو أثر دائم لا يمكن التراجع عنه. لن يكون بمقدور {siteName} استعادة حسابك و لا البيانات التي حذفت.",
|
||||
"account.settings.delete.account.modal.text.2": "إن واصلت، فلن تستطيع استخدام هذا الحساب لمتابعة المساقات على {siteName}.",
|
||||
"account.settings.delete.account.modal.text.2.edX": "إن واصلت، فلن تستطيع استخدام هذا الحساب لمتابعة المساقات على على تطبيق edX و لا edx.org و لا أي موقع آخر تستضيفه edX. وهذا يشمل الوصول إلى edx.org من نظام صاحب العمل أو الجامعة و الوصول إلى المواقع الخاصة التي يقدمها MIT Open Learning و Wharton Executive Education و Harvard Medical School.",
|
||||
"account.settings.delete.account.modal.enter.password": "إن كنت لا تزال ترغب في المتابعة و حذف حسابك، فيرجى إدخال كلمة المرور:",
|
||||
"account.settings.delete.account.modal.confirm.delete": "تعم، احذف",
|
||||
"account.settings.delete.account.modal.confirm.cancel": "لا",
|
||||
"account.settings.delete.account.error.unable.to.delete": "لم نستطع حذف الحساب",
|
||||
"account.settings.delete.account.error.no.password": "كلمة المرور مطلوبة",
|
||||
"account.settings.delete.account.error.invalid.password": "كلمة المرور غير صحيحة",
|
||||
"account.settings.delete.account.error.unable.to.delete.details": "عذراً، حدث خطأ أثناء محاولة معالجة طلبك. رجاءً أعد المحاولة لاحقًا.",
|
||||
"account.settings.delete.account.modal.after.header": "نأسف لذهابك! سيُحذف حسابك في ظرف وجيز.",
|
||||
"account.settings.delete.account.modal.after.text": "حذف الحساب، بما في ذلك من إزالة من القوائم البريدية، إجراء قد يستغرق بضعة أسابيع حتى يكتمل عبر نظامنا. إن كنت تريد قبل ذلك الحين إيقاف تلقي البريد الإلكتروني، فيرجى إلغاء الاشتراك من تذييل أي بريد إلكتروني.",
|
||||
"account.settings.delete.account.modal.after.button": "إغلاق ",
|
||||
"account.settings.delete.account.text.3.edX": "قد تفقد كذلك إمكانية الوصول إلى الشهادات الموثقة و كذا مؤهلات البرامج الأخرى مثل شهادات MicroMasters. يمكنك الاحتفاظ بنسخة عنها لديك قبل المواصلة إلى الحذف. {actionLink}.",
|
||||
"account.settings.delete.account.text.3": "قد تفقد كذلك إمكانية الوصول إلى الشهادات الموثقة و كذا مؤهلات البرامج الأخرى. يمكنك الاحتفاظ بنسخة عنها لديك قبل المواصلة إلى الحذف.",
|
||||
"account.settings.message.demographics.service.issue": "حدث خطأ أثناء محاولة استخراج أو حفظ معلومات حسابك. رجاءً أعد المحاولة لاحقًا.",
|
||||
"account.settings.field.demographics.gender": "هوية الجنس",
|
||||
"account.settings.field.demographics.gender.empty": "إضافة هوية الجنس",
|
||||
"account.settings.field.demographics.gender.options.empty": "حدد هوية الجنس",
|
||||
"account.settings.field.demographics.gender_description": "وصف هوية الجنس",
|
||||
"account.settings.field.demographics.gender_description.empty": "أدخل الوصف",
|
||||
"account.settings.field.demographics.ethnicity": "هوية العرق/ الأصل",
|
||||
"account.settings.field.demographics.ethnicity.empty": "إضافة هوية العرق / الأصل",
|
||||
"account.settings.field.demographics.ethnicity.options.empty": "اختر كل ما ينطبق",
|
||||
"account.settings.field.demographics.income": "دخل الأسرة",
|
||||
"account.settings.field.demographics.income.empty": "إضافة دخل الأسرة",
|
||||
"account.settings.field.demographics.income.options.empty": "حدد نطاقًا لدخل الأسرة",
|
||||
"account.settings.field.demographics.military_history": "الوضعية إزاء الخدمة العسكرية في الولايات المتحدة",
|
||||
"account.settings.field.demographics.military_history.empty": "إضافة الوضعية إزاء الخدمة العسكرية",
|
||||
"account.settings.field.demographics.military_history.options.empty": "اختر الوضعية إزاء الخدمة العسكرية",
|
||||
"account.settings.field.demographics.learner_education_level": "مستواك التعليمي",
|
||||
"account.settings.field.demographics.learner_education_level.empty": "إضافة المستوى التعليمي",
|
||||
"account.settings.field.demographics.parent_education_level": "مستوى الوالدين/الأولياء التعليمي",
|
||||
"account.settings.field.demographics.parent_education_level.empty": "إضافة المستوى التعليمي",
|
||||
"account.settings.field.demographics.education_level.options.empty": "حدد المستوى التعليمي",
|
||||
"account.settings.field.demographics.work_status": "الحالة الوظيفية",
|
||||
"account.settings.field.demographics.work_status.empty": "إضافة الحالة الوظيفية",
|
||||
"account.settings.field.demographics.work_status.options.empty": "حدد الحالة الوظيفية",
|
||||
"account.settings.field.demographics.work_status_description": "وصف الحالة الوظيفية",
|
||||
"account.settings.field.demographics.work_status_description.empty": "أدخل الوصف",
|
||||
"account.settings.field.demographics.current_work_sector": "مجال العمل الحالي",
|
||||
"account.settings.field.demographics.current_work_sector.empty": "إضافة مجال العمل",
|
||||
"account.settings.field.demographics.future_work_sector": "مجال العمل المستقبلي",
|
||||
"account.settings.field.demographics.future_work_sector.empty": "إضافة مجال العمل",
|
||||
"account.settings.field.demographics.work_sector.options.empty": "حدد مجال العمل",
|
||||
"account.settings.section.demographics.why": "ما هي غاية {siteName} من جمع هذه المعلومات؟",
|
||||
"account.settings.name.change.title.id": "تغيير الاسم هذا يتطلب التحقق من الهوية",
|
||||
"account.settings.name.change.title.begin": "قبل أن نبدأ",
|
||||
"account.settings.name.change.warning.one": "تحذير: يقوم هذا الإجراء بتحديث الاسم الذي يظهر على جميع الشهادات التي تم الحصول عليها على هذا الحساب في الماضي و أي شهادات تحصل عليها حاليا أو مستقبلاً.",
|
||||
"account.settings.name.change.warning.two": "لا يمكن التراجع عن هذا الإجراء دون التحقق من هويتك.",
|
||||
"account.settings.name.change.id.name.label": "أدخل اسمك كما يظهر في بطاقة تعريف الطالب أو العمل أو بطاقة الهوية الصادرة عن الحكومة.",
|
||||
"account.settings.name.change.id.name.placeholder": "أدخل الاسم الموجود في بطاقة تعريفك ذات الصورة.",
|
||||
"account.settings.name.change.error.valid.name": "رجاءً أدخل اسما صحيحا.",
|
||||
"account.settings.name.change.error.general": "حدث خطأ تقني. رجاءً حاول مجددًا.",
|
||||
"account.settings.name.change.continue": "مواصلة",
|
||||
"account.settings.name.change.cancel": "إلغاء",
|
||||
"error.notfound.message": "الصفحة التي تبحث عنها غير متوفرة أو هناك خطأ في العنوان. رجاءً تحقق من العنوان و حاول مجددًا.",
|
||||
"account.settings.editable.field.password.reset.button.confirmation.support.link": "الدعم الفني",
|
||||
"account.settings.editable.field.password.reset.button.confirmation": "لقد أرسلنا رسالة إلى {email}. انقر على الرابط في الرسالة لإعادة ضبط كلمة المرور الخاصة بك. لم تصلك الرسالة؟ اتصل بـ{technicalSupportLink}.",
|
||||
"account.settings.editable.field.password.reset.button": "إعادة ضبط كلمة المرور",
|
||||
"account.settings.editable.field.password.reset.button.forbidden": "طلبك السابق في تقدّم، رجاءً حاول مجددًا بعد لحظات قليلة.",
|
||||
"account.settings.editable.field.password.reset.label": "كلمة المرور",
|
||||
"account.settings.sso.link.account": "تسجيل الدخول باستخدام {name}",
|
||||
"account.settings.sso.account.connected": "موصول",
|
||||
"account.settings.sso.account.disconnect.error": "حدثت مشكلة أثناء فصل هذا الحساب، اتصل بالدعم إن استمرت المشكلة.",
|
||||
"account.settings.sso.unlink.account": "فصل حساب {name}",
|
||||
"account.settings.sso.no.providers": "لا يمكن وصل أي حسابات في الوقت الراهن.",
|
||||
"account.page.title": "الحساب | {siteName}",
|
||||
"id.verification.access.blocked.denied": "لا يمكننا التحقق من هويتك في الوقت الراهن. إن لم تكن قد فعّلت حسابك بعد، فيرجى تفقد مجلد الرسائل غير المرغوب فيها بحثًا عن بريد التفعيل الإلكتروني من {email}.",
|
||||
"id.verification.next": "التالي",
|
||||
"id.verification.support": "الدعم",
|
||||
"id.verification.example.card.alt": "مثال بطاقة تعريف صحيحة بالاسم الكامل والصورة.",
|
||||
"id.verification.requirements.title": "متطلبات التحقق باستخدام الصورة",
|
||||
"id.verification.requirements.description": "لإكمال التحقق بالصورة، ستحتاج ما يلي:",
|
||||
"id.verification.requirements.card.device.title": "جهاز مزود بكاميرا",
|
||||
"id.verification.requirements.card.device.allow": "السماح",
|
||||
"id.verification.requirements.card.id.title": "بطاقة تعريف بها صورة",
|
||||
"id.verification.requirements.card.id.text": "تحتاج إلى بطاقة تعريف صالحة تحتوي اسمك الكامل وصورتك، كرخصة القيادة أو جواز السفر مثلاً.",
|
||||
"id.verification.privacy.title": "معلومات الخصوصية",
|
||||
"id.verification.privacy.need.photo.question": "ما حاجة {siteName} لصورتي؟",
|
||||
"id.verification.privacy.need.photo.answer": "نستخدم صور التحقق الخاصة بك لتأكيد هويتك ولضمان صحة شهادتك.",
|
||||
"id.verification.privacy.do.with.photo.question": "ماذا يُفعَل بهذه الصورة في {siteName}؟",
|
||||
"id.verification.privacy.do.with.photo.answer": "نقوم بتشفير صورتك بشكل آمن وإرسالها إلى خدمة الترخيص الخاصة بنا للمراجعة. صورتك و معلوماتك لا تُحفَظ و لا تظهر في أي مكان على {siteName} بعد اكتمال عملية التحقق.",
|
||||
"id.verification.access.blocked.title": "التحقق من الهوية",
|
||||
"id.verification.access.blocked.enrollment": "أنت حاليا غير ملتحق بأي مساق يتطلب التحقق من الهوية.",
|
||||
"id.verification.access.blocked.pending": "لقد سلّمت من قبل معلومات التحقق الخاصة بك. ستصلك رسالة على لوحة المعلومات الخاصة بك عند اكتمال عملية التحقق (عادةً في ظرف 5 أيام).",
|
||||
"id.verification.photo.take": "التقاط صورة ",
|
||||
"id.verification.photo.retake": "إعادة التقاط الصورة؟",
|
||||
"id.verification.photo.enable.detection": "تفعيل اكتشاف الوجوه",
|
||||
"id.verification.photo.enable.detection.portrait.help.text": "في حال التأشير، سيبرز مربع حول وجهك. يمكن رؤية وجهك بوضوح إن كان المربع المحيط به أزرق اللون. أما إن كان وجهك في وضع غير جيد أو غير قابل للاكتشاف، فسيكون المربع أحمر اللون.",
|
||||
"id.verification.photo.enable.detection.id.help.text": "في حال التأشير، سيظهر مربع حول صورة وجهك في بطاقة الهوية. يمكن رؤية وجهك بوضوح إن كان المربع المحيط به أزرق اللون. أما إن كان وجهك في وضع غير جيد أو غير قابل للاكتشاف، فسيكون المربع أحمر اللون.",
|
||||
"id.verification.photo.feedback.correct": "موضع الوجه جيد.",
|
||||
"id.verification.photo.feedback.two.faces": "تم اكتشاف أكثر من وجه واحد.",
|
||||
"id.verification.photo.feedback.no.faces": "لم يتم اكتشاف أي وجه.",
|
||||
"id.verification.photo.feedback.top.left": "الموضع خاطئ. أعلى اليسار.",
|
||||
"id.verification.photo.feedback.top.center": "الموضع خاطئ. أعلى الوسط.",
|
||||
"id.verification.photo.feedback.top.right": "الموضع خاطئ. أعلى اليمين.",
|
||||
"id.verification.photo.feedback.center.left": "الموضع خاطئ. وسط اليسار.",
|
||||
"id.verification.photo.feedback.center.center": "الموضع خاطئ. قريب جدًا من الكاميرا.",
|
||||
"id.verification.photo.feedback.center.right": "الموضع خاطئ. وسط اليمين.",
|
||||
"id.verification.photo.feedback.bottom.left": "الموضع خاطئ. أسفل اليسار.",
|
||||
"id.verification.photo.feedback.bottom.center": "الموضع خاطئ. أسفل الوسط.",
|
||||
"id.verification.photo.feedback.bottom.right": "الموضع خاطئ. أسفل اليمين.",
|
||||
"id.verification.camera.access.title": "صلاحيات الكاميرا",
|
||||
"id.verification.camera.access.title.success": "الوصول للكاميرا ممكن",
|
||||
"id.verification.camera.access.title.failed": "تعذّر الوصول للكاميرا.",
|
||||
"id.verification.camera.access.click.allow": "رجاءً تأكد من النقر على \"السماح\"",
|
||||
"id.verification.camera.access.enable": "تفعيل الكاميرا",
|
||||
"id.verification.camera.access.problems": "لديك مشاكل؟",
|
||||
"id.verification.camera.access.skip": "تَخَطَّ و قم برفع ملفات صور بدلاً من ذلك.",
|
||||
"id.verification.camera.access.success": "يبدو أن كاميرا جهازك جاهزة و تعمل.",
|
||||
"id.verification.camera.access.failure": "يبدو أننا غير قادرين على الوصول إلى كاميرا جهازك. ستحتاج لرفع ملف صورتك و صورة بطاقة هويتك.",
|
||||
"id.verification.camera.access.failure.temporary": "يبدو أننا غير قادرين على الوصول إلى الكاميرا. رجاءً تحقق من أن كاميرا جهازك متصلة ومن أنك قد سمحت للمتصفح بالوصول إليها.",
|
||||
"id.verification.camera.access.failure.temporary.chrome": "لتفعيل الوصول للكاميرا في متصفح كروم:",
|
||||
"id.verification.camera.access.failure.temporary.chrome.step1": "افتح كروم.",
|
||||
"id.verification.camera.access.failure.temporary.chrome.step2": "توجه إلى المزيد > الإعدادات.",
|
||||
"id.verification.camera.access.failure.temporary.chrome.step2.windows": "لنظام ويندوز: Alt+F أو Alt+E أو F10 متبوعاً بـ SPACEBAR",
|
||||
"id.verification.camera.access.failure.temporary.chrome.step2.mac": "لنظام ماك: Command+,",
|
||||
"id.verification.camera.access.failure.temporary.chrome.step3": "تحت علامة التبويب \"الخصوصية والأمان\"، اختر \"إعدادات الموقع\" ثم \"الكاميرا\".",
|
||||
"id.verification.camera.access.failure.temporary.chrome.step4": "ضمن \"قائمة الحظر\"، ابحث عن \"edx.org\" وحدده.",
|
||||
"id.verification.camera.access.failure.temporary.chrome.step5": "في قسم \"الصلاحيات\"، قم بتحديث صلاحيات الكاميرا إلى \"السماح\".",
|
||||
"id.verification.camera.access.failure.temporary.ie11": "لتفعيل الوصول للكاميرا في متصفح انترنت إكسبلورر:",
|
||||
"id.verification.camera.access.failure.temporary.ie11.step1": "افتح إدارة إعدادات Flash Player عن طريق الانتقال إلى إعدادات Windows > لوحة التحكم > Flash Player.",
|
||||
"id.verification.camera.access.failure.temporary.ie11.step2": "حدد علامة التبويب \"Camera and Mic\" (الكاميرا والميكروفون)، ثم حدد الزر \"Camera and Microphone Settings by Site\" (إعدادات الكاميرا والميكروفون حسب الموقع).",
|
||||
"id.verification.camera.access.failure.temporary.ie11.step3": "اختر \"edx.org\" من قائمة مواقع ويب وقم بتغيير الصلاحيات من خلال تحديد \"السماح\" في القائمة المنسدلة.",
|
||||
"id.verification.camera.access.failure.temporary.firefox": "لتمكين الوصول للكاميرا في متصفّح فايرفوكس: ",
|
||||
"id.verification.camera.access.failure.temporary.firefox.step1": "فتح متصفّح فايرفوكس.",
|
||||
"id.verification.camera.access.failure.temporary.firefox.step2": "أدخل \"about:preferences\" في شريط عنوان الرابط.",
|
||||
"id.verification.camera.access.failure.temporary.firefox.step3": "حدد علامة التبويب \"الخصوصية والأمان\"، وانتقل إلى القسم \"الصلاحيات\".",
|
||||
"id.verification.camera.access.failure.temporary.firefox.step4": "بجوار \"Camera\" (الكاميرا)، حدد \"Settings…\" (الإعدادات…) الزر.",
|
||||
"id.verification.camera.access.failure.temporary.firefox.step5": "في شريط البحث أدخل \"edx.org.\"",
|
||||
"id.verification.camera.access.failure.temporary.firefox.step6": "في عمود الحالة لـ \"edx.org,\" حدد \"السماح\" من القائمة المنسدلة.",
|
||||
"id.verification.camera.access.failure.temporary.firefox.step7": "اختر \"حفظ التغييرات.\"",
|
||||
"id.verification.camera.access.failure.temporary.safari": "تمكين الوصول للكاميرا في متصفّح سفاري: ",
|
||||
"id.verification.camera.access.failure.temporary.safari.step1": "فتح متصفّح سفاري.",
|
||||
"id.verification.camera.access.failure.temporary.safari.step2": "انقر فوق قائمة تطبيق سفاري، ثم حدد \"Preferences\" (التفضيلات). يمكنك أيضاً استخدام الأمرCommand+, كاختصار للوحة المفاتيح",
|
||||
"id.verification.camera.access.failure.temporary.safari.step3": "حدد علامة التبويب \"مواقع الويب\" ثم حدد \"كاميرا\".",
|
||||
"id.verification.camera.access.failure.temporary.safari.step4": "حدد \"edx.org\" وقم بتغيير صلاحيات الكاميرا إلى \"السماح\".",
|
||||
"id.verification.camera.access.failure.unsupported": "يبدو أن متصفحك لا يدعم الوصول إلى الكاميرا.",
|
||||
"id.verification.camera.access.failure.unsupported.chrome.explanation": "لا يدعم متصفح Chrome حاليًا الوصول إلى الكاميرا على أجهزة iOS مثل أجهزة iPhone و iPad.",
|
||||
"id.verification.camera.access.failure.unsupported.instructions": "رجاءً استخدم متصفحًا آخر لإكمال التحقق من الهوية.",
|
||||
"id.verification.photo.tips.title": "نصائح مفيدة بخصوص الصورة",
|
||||
"id.verification.photo.tips.description": "بعد ذلك، سنحتاج منك التقاط صورة لوجهك. يرجى مراجعة النصائح المفيدة أدناه.",
|
||||
"id.verification.photo.tips.list.title": "نصائح بخصوص الصورة",
|
||||
"id.verification.photo.tips.list.description": "لالتقاط صورة ناجحة، يُرجى التأكّد ممّا يلي:",
|
||||
"id.verification.photo.tips.list.well.lit": "أنّ وجهك مُضاء جيدًا.",
|
||||
"id.verification.photo.tips.list.inside.frame": "أنّ وجهك كلَّه داخل إطار الصورة.",
|
||||
"id.verification.portrait.photo.title.camera": "التقط صورة لنفسك",
|
||||
"id.verification.portrait.photo.instructions.camera": "عندما يكون وجهك في موضعه، استخدم زر 'التقاط صورة' أدناه لالتقاط صورتك.",
|
||||
"id.verification.camera.help.sight.question": "ماذا إن لم أتمكن من رؤية صورة الكاميرا أو إن لم أتمكن من رؤية صورتي لتحديد أي جانب مرئي؟",
|
||||
"id.verification.camera.help.sight.answer.portrait": "قد تتمكن من إكمال إجراء التقاط الصور دون مساعدة، لكن قد يتطلب الأمر بضع محاولات لضبط موضع الكاميرا بشكل صحيح.يختلف موضع الكاميرا المثالي من حاسوب ﻵخر، لكن عمومًا يكون أفضل موضع لتصوير الرأس تقريبًا على بعد 12-18 بوصة (30-45 سنتمترًا) من الكاميرا، مع وضع رأسك في المنتصف بالنسبة إلى شاشة الحاسوب. إن تم رفض الصور التي ترسلها، فحاول تغيير اتجاه الكمبيوتر أو الكاميرا لتغيير زاوية الإضاءة.",
|
||||
"id.verification.camera.help.sight.answer.id": "قد تتمكن من إكمال إجراء التقاط الصور دون مساعدة، لكن قد يتطلب الأمر بضع محاولات لضبط موضع الكاميرا بشكل صحيح.يختلف موضع الكاميرا المثالي من حاسوب ﻵخر، لكن عمومًا يكون أفضل موضع لتصوير بطاقة تعريف تقريبًا على بعد 8-12 بوصة (20-30 سنتمترًا) من الكاميرا، مع وضع البطاقة في المنتصف بالنسبة للكاميرا. إن تم رفض الصور التي ترسلها، فحاول تغيير اتجاه الكمبيوتر أو الكاميرا لتغيير زاوية الإضاءة. إن أكثر سبب للرفض هو عدم القدرة على قراءة نص بطاقة التعريف.",
|
||||
"id.verification.camera.help.difficulty.question.portrait": "ماذا لو واجهت صعوبة في تثبيت رأسي في الموضع المناسب للكاميرا؟",
|
||||
"id.verification.camera.help.difficulty.question.id": "ماذا لو واجهت صعوبة في تثبيت بطاقة هويتي في الموضع المناسب للكاميرا؟",
|
||||
"id.verification.camera.help.difficulty.answer": "إن احتجت لمساعدة في التقاط صورة لإرسالها، فاتصل بدعم {siteName} للحصول على اقتراحات إضافية.",
|
||||
"id.verification.id.photo.unclear.question": "هل صورة بطاقة تعريفك غير واضحة أو ضبابية جدًا؟",
|
||||
"id.verification.id.tips.title": "نصائح مفيدة بخصوص بطاقة التعريف",
|
||||
"id.verification.id.tips.description": "بعد ذلك، عليك التقاط صورة لبطاقة تعريف صالحة تتضمن اسمك الكامل مع صورة، مثل رخصة القيادة أو جواز السفر.يرجى منك تجهيز بطاقة تعريفك.",
|
||||
"id.verification.id.tips.list.well.lit": "أن تكون بطاقة تعريفك مضاءة جيدًا.",
|
||||
"id.verification.id.tips.list.clear": "تأكد من قدرتك على رؤية صورتك وقراءة اسمك بوضوح.",
|
||||
"id.verification.id.photo.title.camera": "التقط صورة لبطاقة تعريفك",
|
||||
"id.verification.id.photo.title.upload": "ارفع صورة لبطاقة تعريفك",
|
||||
"id.verification.id.photo.preview.alt": "معاينة صورة الهوية.",
|
||||
"id.verification.id.photo.instructions.camera": "عندما تكون بطاقتك في موضعها، استخدم زر 'التقاط صورة' أدناه لالتقاط صورتك. يرجى استخدام جواز سفر أو رخصة قيادة أو بطاقة تعريف أخرى تتضمن اسمك الكامل وصورة لوجهك.",
|
||||
"id.verification.id.photo.instructions.upload": "يرجى رفع صورة لبطاقة تعريفك. تأكد من أن البطاقة كاملة داخل الإطار و أنها مضاءة جيدًا. يجب أن يكون حجم الملف أقل من 10 ميجابايت. الأنساق المدعومة:",
|
||||
"id.verification.id.photo.instructions.upload.error.invalidFileType": "الملف الذي حددته ليس ضمن أنواع الصورة المدعومة. اختر رجاءً من بين الأنساق التالية:",
|
||||
"id.verification.id.photo.instructions.upload.error.fileTooLarge": "الملف الذي حددته كبير جداً. رجاءً أعد المحاولة باستخدام ملف ذي حجم أقل من 10 ميجابايت.",
|
||||
"id.verification.name.check.title": "تحقق مرة أخرى من اسمك",
|
||||
"id.verification.name.check.instructions": "هل الاسم أدناه يطابق الاسم الموجود في بطاقة تعريفك ذات الصورة. إن لم يكن كذلك، فقم بتحديث الاسم أدناه ليطابق بطاقة تعريفك.",
|
||||
"id.verification.name.check.mismatch.information": "إن كان الاسم أدناه لا يطابق بطاقة تعريفك، فسيتم رفض تأكيد هويتك.",
|
||||
"id.verification.name.error": "رجاءً أدخل اسمك كما يظهر في بطاقة تعريفك ذات الصورة.",
|
||||
"id.verification.account.name.warning.prefix": "تُرجى الملاحظة:",
|
||||
"id.verification.account.name.settings": "إعدادات الحساب",
|
||||
"id.verification.name.label": "الاسم",
|
||||
"id.verification.account.name.photo.alt": "صورة بطاقة هويتك التي ستُسلَّم.",
|
||||
"id.verification.review.title": "مراجعة صورك",
|
||||
"id.verification.review.description": "يُرجى التأكّد من أنّ الصور والمعلومات التي قدّمتها تمكّننا من التحقّق من هويّتك. ",
|
||||
"id.verification.review.portrait.label": "صورتك الشخصية",
|
||||
"id.verification.review.portrait.alt": "صورة وجهك التي ستُسلَّم.",
|
||||
"id.verification.review.portrait.retake": "إعادة التقاط الصورة الشخصية",
|
||||
"id.verification.review.id.label": "بطاقة تعريفك",
|
||||
"id.verification.review.id.alt": "صورة بطاقة تعريفك التي ستُسلَّم.",
|
||||
"id.verification.review.id.retake": "إعادة التقاط صورة بطاقة الهوية",
|
||||
"id.verification.review.confirm": "تسليم",
|
||||
"id.verification.submission.alert.error.face": "مطلوبة صورة لوجهك. رجاءً أعد التقاط صورتك الشخصية.",
|
||||
"id.verification.submission.alert.error.id": "مطلوبة صورة لبطاقة تعريفك. رجاءً أعد التقاط صورة لبطاقة تعريفك.",
|
||||
"id.verification.submission.alert.error.name": "مطلوب اسم حساب صحيح. يرجى تحديث اسم حسابك لمطابقة الاسم على بطاقة تعريفك.",
|
||||
"id.verification.submission.alert.error.unsupported": "واحد أو أكثر من الملفات التي قمت برفعها في نَسقٍ غير مدعوم. رجاءً اختر مما يلي:",
|
||||
"id.verification.review.error": "{siteName} صفحة دعم",
|
||||
"id.verification.submitted.title": "التحقق من الهوية جارٍ",
|
||||
"id.verification.submitted.text": "لقد تلقينا معلوماتك و نحن الآن نتحقق من هويتك. سيتم إخطارك عند اكتمال عملية التحقق (عادةً في ظرف 5 أيام). إلى ذلكم الحين، لا يزال يمكنك الوصول لجميع محتويات المساق المتاحة.",
|
||||
"id.verification.return.dashboard": "العودة للوحة المعلومات",
|
||||
"id.verification.return.course": "العودة للمساق",
|
||||
"id.verification.return.generic": "العودة",
|
||||
"id.verification.photo.upload.help.title": "قم يدلا من هذا برفع صورة",
|
||||
"id.verification.photo.camera.help.title": "استخدم الكاميرا بدلاً من هذا",
|
||||
"id.verification.photo.upload.help.text": "إن واجهتك مشكلة في استخدام مُلتقط الصور أعلاه، فقد ترغب بدﻷ من ذلك في رفع صورة. لرفع صورة، انقر على الزر أدناه.",
|
||||
"id.verification.photo.camera.help.text": "إن واجهتك مشكلة في رفع صورة أعلاه، فقد ترغب بدلا من ذلك في استخدام كاميرا جهازك. لاستخدام الكاميرا، انقر على الزر أدناه.",
|
||||
"id.verification.upload.help.button": "انتقل إلى وضع الرفع",
|
||||
"id.verification.camera.help.button": "انتقل إلى وضع الكاميرا",
|
||||
"id.verification.request.camera.access.instructions": "حتى تلتقط صورة باستخدام الكاميرا، قد تتلقى طلبًا من المتصفح للوصول إلى الكاميرا. {clickAllow}",
|
||||
"id.verification.requirements.account.managed.alert": "إعدادات حسابك يديرها {managerTitle}. إن لم يكن الاسم في بطاقة هويتك ذات الصورة. مطابقًا للاسم الذي في حسابك، فيرجى الاتصال بالمسؤول {profileDataManager} أو ب{support} للحصول على المساعدة قبل إتمام عملية التحقق من الصورة..",
|
||||
"id.verification.requirements.card.device.text": "أنت بحاجة إلى جهاز مزود بكاميرا. إذا تلقيت طلبًا من المتصفح للوصول إلى كاميرا جهازك، فتأكد رجاءً من النقر على {السماح}.",
|
||||
"id.verification.account.name.summary.alert": "إعدادات حسابك يديرها {managerTitle}. إن لم يكن الاسم في بطاقة هويتك ذات الصورة. مطابقًا للاسم الذي في حسابك، فيرجى الاتصال بالمسؤول {profileDataManager} أو ب{support} للحصول على المساعدة قبل إتمام عملية التحقق من الصورة..",
|
||||
"idv.submission.alert.error": "\nواجهنا خطأ فني أثناء محاولة رفع طلب التحقق من الهوية\nقد تكون هذه مشكلة مؤقتة، لذا يرجى المحاولة مجددًا بعد بضع دقائق\nإن استمرت المشكلة، فيرجى الذهاب إلى {support_link} للحصول على المساعدة.",
|
||||
"id.verification.account.name.edit": "تعديل {sr}",
|
||||
"notification.preference.heading": "Notifications",
|
||||
"notification.preference.app.title": "{\n key, select,\n discussion {Discussions}\n coursework {Course Work}\n other {{key}}\n }",
|
||||
"notification.preference.title": "{\n text, select,\n core {Core notifications}\n newDiscussionPost {New discussion posts}\n newQuestionPost {New question posts}\n other {{text}}\n }",
|
||||
"notification.preference.type.label": "Type",
|
||||
"notification.preference.web,label": "Web",
|
||||
"notification.preference.help.email": "Email",
|
||||
"notification.preference.help.push": "Push",
|
||||
"notification.preference.load.more.courses": "Load more courses"
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user