Compare commits

..

339 Commits

Author SHA1 Message Date
edX requirements bot
a1c35f134c chore: update browserslist DB (#1421)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2026-03-16 00:53:02 +00:00
renovate[bot]
41af76f027 fix(deps): update dependency qs to v6.15.0 (#1418)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-09 04:52:00 +00:00
edX requirements bot
57a3b0963c chore: update browserslist DB (#1417)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2026-03-09 00:47:58 +00:00
Emad Rad
92dcdd0a98 fix: update button labels in ConfirmationModal for better localization
Close #1415
2026-03-03 15:23:57 -03:00
edX requirements bot
29f0cefc1e chore: update browserslist DB (#1414)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2026-03-02 00:50:06 +00:00
renovate[bot]
794a1d57b9 fix(deps): update dependency bowser to v2.14.1 (#1411)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-23 22:37:50 +00:00
edX requirements bot
fe46e8a1a6 chore: update browserslist DB (#1413)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2026-02-23 21:07:21 +00:00
renovate[bot]
a525d3c22e fix(deps): update dependency core-js to v3.48.0 (#1412)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-23 21:00:37 +00:00
renovate[bot]
81a2c3c0d2 chore(deps): update dependency @edx/frontend-platform to v8.5.5 (#1410)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-16 05:49:56 +00:00
renovate[bot]
0018eafdcc chore(deps): update dependency @edx/browserslist-config to v1.5.1 (#1409)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-16 05:49:31 +00:00
renovate[bot]
e7db2ef753 fix(deps): update dependency qs to v6.14.2 [security] (#1408)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-16 01:12:05 +00:00
edX requirements bot
2e986d9b74 chore: update browserslist DB (#1407)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2026-02-16 00:59:58 +00:00
Brian Smith
7c85195a27 fix(deps): regenerate package-lock.json (#1405)
* fix(deps): regenerate package-lock.json

Co-Authored-By: Claude Code <noreply@anthropic.com>

* test: update snapshots

Co-Authored-By: Claude Code <noreply@anthropic.com>

---------

Co-authored-by: Claude Code <noreply@anthropic.com>
2026-02-12 09:41:39 -05:00
edX requirements bot
b686acf5f5 chore: update browserslist DB (#1406)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2026-02-09 00:48:51 +00:00
Anton Melser
166deeafbd docs: Generify currently supported node version 2026-01-27 09:57:11 -03:00
renovate[bot]
f33fe7d0e5 chore(deps): update dependency @edx/frontend-platform to v8.5.4 (#1404)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-26 08:50:30 +00:00
edX requirements bot
e2896dbf94 chore: update browserslist DB (#1403)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2026-01-26 00:42:27 +00:00
renovate[bot]
f07d266a43 chore(deps): update react-router monorepo to v6.30.3 (#1402)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-19 06:45:57 +00:00
edX requirements bot
69cdc5f191 chore: update browserslist DB (#1392)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2026-01-19 00:41:31 +00:00
Awais Ansari
4f51f71acc feat: implemented notifications configurations V3 API (#1401)
* feat: implemented notifications configurations V3 API

* fix: removed default daily email cadence when email toggle is turned on
2026-01-15 18:56:02 +05:00
renovate[bot]
c70eca1fde fix(deps): update dependency qs to v6.14.1 [security] (#1400)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-13 20:21:35 +00:00
Diana Villalvazo
39dc5bbbd2 docs: update owner/maintainer (#1398) 2026-01-13 13:27:29 -06:00
renovate[bot]
7fa61f3714 fix(deps): update dependency bowser to v2.13.1 (#1393)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-13 18:43:37 +00:00
Diana Villalvazo
8ce7c1599d test: fix X/twitter broken tests (#1399) 2026-01-13 13:39:44 -05:00
Stanislav
03bdcff331 feat: Change Twitter to X (#1215) 2025-12-02 21:46:31 +05:00
Awais Ansari
d73d840e93 feat: added env parse to boolean functionality (#1389) 2025-12-01 19:39:53 +05:00
edX requirements bot
d23b5f53df chore: update browserslist DB (#1388)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2025-12-01 00:44:00 +00:00
Awais Ansari
da98bfa021 refactor: simplify notifications channels flag logic (#1381) 2025-11-24 16:20:58 +05:00
renovate[bot]
c37640aa69 fix(deps): update dependency core-js to v3.47.0 (#1384)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-24 05:10:14 +00:00
renovate[bot]
ea35227389 fix(deps): update dependency bowser to v2.13.0 (#1383)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-24 05:10:00 +00:00
edX requirements bot
c35bf95c1c chore: update browserslist DB (#1382)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2025-11-24 00:39:08 +00:00
renovate[bot]
bd26928154 chore(deps): update react-router monorepo to v6.30.2 (#1379)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-17 05:47:51 +00:00
edX requirements bot
cdc8efe17b chore: update browserslist DB (#1378)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2025-11-17 00:37:06 +00:00
edX requirements bot
8b6535ea58 chore: update browserslist DB (#1372)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2025-11-10 00:37:40 +00:00
renovate[bot]
4c9498971a fix(deps): update dependency redux-saga to v1.4.2 (#1370)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-03 06:43:39 +00:00
edX requirements bot
a6b6a3f940 chore: update browserslist DB (#1369)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2025-11-03 00:37:33 +00:00
renovate[bot]
cab1a24e10 chore(deps): update dependency @edx/frontend-platform to v8.5.2 (#1367)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-27 06:51:56 +00:00
renovate[bot]
f6b7782d24 chore(deps): update dependency @edx/frontend-component-footer to v14.9.3 (#1366)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-27 06:51:47 +00:00
edX requirements bot
c7bbe8d0d1 chore: update browserslist DB (#1363)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2025-10-20 00:37:12 +00:00
renovate[bot]
20fd7ea13b chore(deps): update dependency @openedx/paragon to v23.14.8 (#1361)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-13 00:33:36 -04:00
renovate[bot]
d55d38ec12 fix(deps): update dependency core-js to v3.46.0 (#1362)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-13 04:33:28 +00:00
edX requirements bot
e77c6ee74a chore: update browserslist DB (#1360)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2025-10-13 00:36:37 +00:00
renovate[bot]
8cb30bedd8 chore(deps): update dependency @testing-library/jest-dom to v6.9.1 (#1359)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-06 05:59:28 +00:00
renovate[bot]
0ab3f5f669 chore(deps): update dependency @openedx/paragon to v23.14.4 (#1358)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-06 05:59:22 +00:00
edX requirements bot
9eaab9c2e5 chore: update browserslist DB (#1357)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2025-10-06 00:34:08 +00:00
renovate[bot]
ac28626b3c fix(deps): update dependency core-js to v3.45.1 (#1356)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-29 04:51:54 +00:00
renovate[bot]
3f90fea26c chore(deps): update dependency @openedx/paragon to v23.14.3 (#1355)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-29 04:51:46 +00:00
edX requirements bot
a0466852d6 chore: update browserslist DB (#1354)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2025-09-29 00:34:57 +00:00
Feanil Patel
9fad507ada Merge pull request #1353 from openedx/feanil/remove-reactifex-packages
build: remove unused reactifex packages
2025-09-26 16:14:50 -04:00
Feanil Patel
73351fa8e8 build: Drop translation pushing.
This is now handled via the openedx-translations repo so we don't need
this target that uses scripts from the deprecated reactifex repo.
2025-09-26 16:08:41 -04:00
Feanil Patel
c8528a7874 build: remove unused reactifex packages
Remove both reactifex and @edx/reactifex packages from devDependencies
as they are no longer needed. Translation extraction functionality has
been verified to work correctly without these dependencies.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-26 16:08:37 -04:00
bydawen
124909ab74 test: Remove support for Node 20 (#1345)
Co-authored-by: Peter Kulko <93188219+PKulkoRaccoonGang@users.noreply.github.com>
2025-09-26 10:49:44 -03:00
oleksandr.buhaienko
77f66f3afb build: Upgrade to Node 24 2025-09-26 09:17:19 -03:00
renovate[bot]
102288407f chore(deps): update dependency @testing-library/jest-dom to v6.8.0 (#1352)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-22 01:43:38 -04:00
renovate[bot]
dd7c35497e fix(deps): update dependency form-urlencoded to v6.1.6 (#1351)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-22 05:43:31 +00:00
edX requirements bot
8331d37b7f chore: update browserslist DB (#1350)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2025-09-22 00:37:03 +00:00
bydawen
1d12506b01 test: Add Node 24 to CI matrix (#1342) 2025-09-19 13:55:15 -04:00
renovate[bot]
c22c4ec5a6 fix(deps): update dependency @edx/frontend-platform to v8.5.1 (#1349)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-15 05:31:26 +00:00
renovate[bot]
69fc4be952 fix(deps): update dependency @edx/frontend-component-footer to v14.9.2 (#1348)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-15 05:31:11 +00:00
Awais Ansari
9d946eacd8 Revert "Replace of injectIntl with useIntl() 9/10" (#1346) 2025-09-11 22:00:53 +05:00
renovate[bot]
0af0935e86 fix(deps): update dependency @edx/frontend-component-footer to v14.9.1 (#1340)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-08 00:58:49 -04:00
renovate[bot]
fd33842109 fix(deps): update dependency @edx/frontend-component-header to v6.6.1 (#1341)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-08 04:58:43 +00:00
edX requirements bot
13b5f3bc12 chore: update browserslist DB (#1339)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2025-09-08 00:35:49 +00:00
Brian Smith
502ad904ea Merge pull request #1328 from openedx/revert-1314-1295/replaceInjectIntl10of10
Revert "Replace of injectIntl with useIntl() 10/10"
2025-09-04 18:37:24 -04:00
diana-villalvazo-wgu
594ae27c0e test: do not revert increase on coverage 2025-09-04 15:13:07 -06:00
sundasnoreen12
0924cb1ba3 Revert "Replace of injectIntl with useIntl() 10/10" 2025-09-04 15:13:07 -06:00
Samuel Allan
90ee5800b4 fix: update frontend-build to fix install issues (#1335)
Earlier versions of @openedx/frontend-build used on older version of
'sharp', which caused intermittent installation issues. The version of
'sharp' was updated in @openedx/frontend-build to fix these issues, so
the frontend-build version can be updated here, to fix the issues in
this project too. See
https://github.com/openedx/frontend-build/issues/664 and
https://github.com/openedx/frontend-build/pull/665 for more information.

The frontend-build dependency was updated by:

```
npm install --package-lock-only @openedx/frontend-build
```

Private-ref: https://tasks.opencraft.com/browse/BB-9953
2025-09-04 08:49:37 -06:00
Diana Villalvazo
6be87b4a82 refactor: Replace of injectIntl with useIntl() (#1303) 2025-09-02 13:52:23 -04:00
edX requirements bot
2137e985b3 chore: update browserslist DB (#1334)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2025-09-01 00:41:51 +00:00
bydawen
ca93f890e1 fix: long email field on account page (#1024)
* fix: long email field on account page

* fix: Add tooltip to the email field

---------

Co-authored-by: Stanislav Lunyachek <stanislav.lunyachek@raccoongang.com>
2025-08-28 15:07:28 +05:00
Eemaan Amir
6a57622a3c test: added test cases to improve test coverage (#1330)
* test: added test cases to improve test coverage

* fix: updated the link for code coverage

* fix: fixed lint errors

* test: added test cases

* fix: fixed message id
2025-08-26 22:04:22 +05:00
renovate[bot]
3d490c3879 fix(deps): update dependency bowser to v2.12.1 (#1333)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-25 04:50:54 +00:00
renovate[bot]
1e09c83300 fix(deps): update dependency @fortawesome/react-fontawesome to v0.2.6 (#1332)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-25 04:50:43 +00:00
edX requirements bot
b660903836 chore: update browserslist DB (#1331)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2025-08-25 00:36:40 +00:00
renovate[bot]
3d2b8416f9 chore(deps): update dependency @testing-library/jest-dom to v6.7.0 (#1327)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-18 06:57:56 +00:00
renovate[bot]
4afd07201b fix(deps): update dependency @openedx/paragon to v23.14.2 (#1326)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-18 06:57:52 +00:00
edX requirements bot
94ead51915 chore: update browserslist DB (#1317)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2025-08-18 00:39:58 +00:00
Brayan Cerón
587e3f2647 feat: add slot to extend the profile fields (#1254)
* feat: add ExtendedProfileFieldsSlot component

* feat: update ExtendedProfileFieldsSlot documentation and images

* fix: format code in ExtendedProfileFieldsSlot README for consistency

* feat: add error handling to ExtendedProfileFieldsSlot component

* feat: replace ExtendedProfileFieldsSlot with AdditionalProfileFieldsSlot and update documentation

* feat: add Example component for AdditionalProfileFieldsSlot and update README

* fix: update README to reflect correct slot name and widget ID for AdditionalProfileFieldsSlot

* chore: remove unused default_fields.png image
2025-08-13 12:34:12 -04:00
renovate[bot]
3394656ed2 fix(deps): update dependency @edx/frontend-platform to v8.5.0 (#1322)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-11 10:25:30 +00:00
renovate[bot]
66eda1a58d fix(deps): update dependency @edx/frontend-component-header to v6.6.0 (#1321)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-11 10:25:25 +00:00
renovate[bot]
c1ccc8c201 fix(deps): update dependency bowser to v2.12.0 (#1323)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-11 07:37:14 +00:00
renovate[bot]
4ae65cfcee fix(deps): update dependency @openedx/paragon to v23.14.1 (#1320)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-11 07:36:46 +00:00
sundasnoreen12
ad7e2035bc Merge pull request #1314 from WGU-Open-edX/1295/replaceInjectIntl10of10
Replace of injectIntl with useIntl() 10/10
2025-08-11 12:32:26 +05:00
sundasnoreen12
d5d67dbe14 Merge pull request #1307 from WGU-Open-edX/1294/replaceInjectIntl9of10
Replace of injectIntl with useIntl() 9/10
2025-08-11 12:30:53 +05:00
Diana Villalvazo
3423e5efea refactor: Replace of injectIntl with useIntl() (#1306) 2025-08-07 16:35:14 -04:00
Diana Villalvazo
f3d30925a8 refactor: Replace of injectIntl with useIntl() (#1305) 2025-08-07 16:31:34 -04:00
Diana Villalvazo
1ec2c3b262 refactor: Replace of injectIntl with useIntl() (#1304) 2025-08-07 16:27:49 -04:00
Diana Villalvazo
260df228fb Replace of injectIntl with useIntl() 4/10 (#1299)
* refactor: Replace of injectIntl with useIntl()

* fix: improve coverage
2025-08-07 16:24:46 -04:00
Diana Villalvazo
6b740a89c6 refactor: Replace of injectIntl with useIntl() (#1298) 2025-08-07 16:21:34 -04:00
Diana Villalvazo
03f8fdbdc3 refactor: Replace of injectIntl with useIntl() (#1297) 2025-08-07 16:18:20 -04:00
diana-villalvazo-wgu
0b86166a57 test: improve coverage 2025-08-07 13:08:04 -05:00
diana-villalvazo-wgu
46eefa7592 refactor: replace injectIntl 2025-08-07 12:40:04 -05:00
Stanislav
036b4be854 feat: Restore jump nav and content width (#994) 2025-08-07 17:30:39 +05:00
Eemaan Amir
eae0bfdca2 Merge pull request #1318 from openedx/INF-2087
chore: updated the notification section description
2025-08-07 11:29:32 +05:00
eemaanamir
05f5903cbc fix: fixed lint 2025-08-07 11:12:19 +05:00
eemaanamir
725ae950f4 chore: updated the notification section description 2025-08-04 16:46:32 +05:00
Eemaan Amir
38c4f3bad3 Merge pull request #1315 from openedx/INF-2003
chore: cleaned up course level preferences apis
2025-07-31 17:22:59 +05:00
eemaanamir
46acf2a5a4 chore: cleaned up course level preferences apis 2025-07-30 12:37:50 +05:00
wgu-jesse-stewart
e8aafef127 feat: add dev script to package.json (#1313)
* fix: add dev script to package.json

* docs: update readme with dev script
2025-07-29 09:46:38 -04:00
renovate[bot]
cf451770ed fix(deps): update dependency @fortawesome/react-fontawesome to v0.2.3 (#1312)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-28 06:40:00 +00:00
renovate[bot]
d9c3975f26 chore(deps): update dependency @testing-library/jest-dom to v6.6.4 (#1311)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-28 06:39:48 +00:00
diana-villalvazo-wgu
5f42857332 refactor: Replace of injectIntl with useIntl() 2025-07-21 14:38:02 -06:00
renovate[bot]
f6babc2db9 fix(deps): update dependency core-js to v3.44.0 (#1302)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-21 10:33:00 +00:00
renovate[bot]
67a053f3e0 fix(deps): update dependency @edx/frontend-component-header to v6.4.2 (#1301)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-21 07:34:41 +00:00
Diana Villalvazo
845ab30af5 refactor: Replace of injectIntl with useIntl() (#1296) 2025-07-17 16:33:42 -07:00
Muhammad Adeel Tajamul
3244ecf70b feat: added support for non editable field in notification preference (#1284) 2025-07-09 10:00:25 +05:00
renovate[bot]
a1390ebf36 fix(deps): update dependency @openedx/paragon to v23.14.0 (#1283)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-07 07:34:04 +00:00
renovate[bot]
89f9d9511f fix(deps): update dependency @edx/frontend-component-header to v6.4.1 (#1282)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-07 07:33:43 +00:00
edX requirements bot
bc66c74a33 chore: update browserslist DB (#1281)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2025-07-07 00:39:25 +00:00
Ahtisham Shahid
d760be1a53 feat: integrated notification preferences v2 API (#1280) 2025-07-04 17:00:09 +05:00
renovate[bot]
929a34a0f6 fix(deps): update dependency core-js to v3.43.0 (#1278)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-30 06:08:09 +00:00
edX requirements bot
55fc919c6f chore: update browserslist DB (#1277)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2025-06-30 00:39:16 +00:00
Hassan Raza
102a93486e Merge pull request #1271 from openedx/hraza/INF-1917
feat: Add notification for discussion app
2025-06-27 19:23:28 +05:00
renovate[bot]
981ba84163 chore(deps): update dependency @openedx/frontend-build to v14.6.1 (#1275)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-26 23:11:28 +00:00
renovate[bot]
8aa918bfb9 fix(deps): update dependency @openedx/paragon to v23.13.0 (#1276)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-26 19:43:54 +00:00
Adam Stankiewicz
4ca2ab9f4e fix: use Container component with size='xl' (#1274) 2025-06-19 10:07:11 -04:00
Brian Smith
c01f1854ee feat!: add design tokens support (#1272)
BREAKING CHANGE: Pre-design-tokens theming is no longer supported.

Co-authored-by: Diana Olarte <diana.olarte@edunext.co>
2025-06-18 12:35:37 -04:00
Hassan Raza
987484d205 feat: Add notification for discussion app 2025-06-17 14:47:36 +05:00
renovate[bot]
f86496468e fix(deps): update dependency @openedx/paragon to v22.20.2 (#1269)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-16 05:45:50 +00:00
renovate[bot]
e916ba29b9 fix(deps): update dependency @edx/frontend-platform to v8.4.0 (#1268)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-16 05:45:34 +00:00
edX requirements bot
6410ce1d8f chore: update browserslist DB (#1267)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2025-06-16 00:37:49 +00:00
Diana Villalvazo
d0eebfa0ea fix: untranslated user facing texts for i18n (#1245)
Co-authored-by: diana-villalvazo-wgu <dianaximena.villalva@wgu.edu>
2025-06-10 11:57:54 -04:00
renovate[bot]
77daf2fbad fix(deps): update dependency @edx/frontend-component-footer to v14.9.0 (#1264)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-09 05:53:40 +00:00
renovate[bot]
354426037e fix(deps): update dependency @edx/frontend-platform to v8.3.9 (#1263)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-09 05:53:31 +00:00
edX requirements bot
b9af9ed700 chore: update browserslist DB (#1261)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2025-06-09 00:38:12 +00:00
renovate[bot]
2342eaae82 fix(deps): update dependency @edx/frontend-component-footer to v14.8.0 (#1259)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-02 05:42:07 +00:00
renovate[bot]
a1484264fb fix(deps): update dependency @edx/frontend-platform to v8.3.8 (#1258)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-02 05:42:01 +00:00
edX requirements bot
cff4a76b0c chore: update browserslist DB (#1257)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2025-06-02 00:37:24 +00:00
renovate[bot]
3e2e8095b4 fix(deps): update dependency @openedx/paragon to v22.18.1 (#1256)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-26 01:54:28 -04:00
renovate[bot]
ebd63a13a9 fix(deps): update react-router monorepo to v6.30.1 (#1255)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-26 05:54:21 +00:00
Muhammad Adeel Tajamul
fd0d08daa1 feat: added immediate option in email cadence dropdown (#1251)
* feat: added immediate option in email cadence dropdown

* chore: added tests
2025-05-20 15:51:07 +05:00
renovate[bot]
6ae4c2d68b fix(deps): update dependency @edx/frontend-platform to v8.3.7 (#1252)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-19 13:56:27 +00:00
renovate[bot]
95331d1b10 fix(deps): update dependency @edx/frontend-platform to v8.3.6 (#1250)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-19 07:20:20 +00:00
renovate[bot]
7c63b66d8e fix(deps): update dependency @edx/frontend-component-footer to v14.7.2 (#1249)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-19 07:20:07 +00:00
edX requirements bot
810f506e52 chore: update browserslist DB (#1248)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2025-05-19 00:37:19 +00:00
edX requirements bot
33fd669c8f chore: update browserslist DB (#1247)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2025-05-12 00:37:27 +00:00
Muhammad Adeel Tajamul
5c5204fb17 chore: temporarily disabled notification preferences course dropdown (#1246) 2025-05-08 18:18:08 +05:00
renovate[bot]
95db89a9dd chore(deps): update dependency @openedx/frontend-build to v14.6.0 (#1244)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-05 05:28:19 +00:00
renovate[bot]
81a878a658 fix(deps): update dependency @edx/frontend-component-footer to v14.7.1 (#1243)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-05 05:28:06 +00:00
edX requirements bot
b502de846a chore: update browserslist DB (#1242)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2025-05-05 00:37:13 +00:00
renovate[bot]
39f0123820 fix(deps): update dependency @edx/openedx-atlas to ^0.7.0 (#1240)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-28 02:05:08 -04:00
renovate[bot]
7ee70193c0 fix(deps): update dependency @edx/frontend-component-footer to v14.7.0 (#1239)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-28 06:05:00 +00:00
Awais Ansari
0e1574dba7 chore: updated NODE_ENV string in .env (#1238) 2025-04-25 15:21:20 +05:00
Brian Smith
0d45ae6599 feat: import FooterSlot from component package instead of slot package (#1230) 2025-04-24 12:18:00 -04:00
Brian Smith
78246cf26b feat: standardize slot ids (#1237) 2025-04-24 07:28:24 -04:00
renovate[bot]
ca193563ec fix(deps): update dependency @edx/frontend-component-header to v6.4.0 (#1236)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-23 16:13:09 -04:00
Awais Ansari
d0efd35e66 Revert "fix: removed extra api call and strict mode (#1234)" (#1235)
This reverts commit 375b704eef.
2025-04-23 15:42:58 -04:00
sundasnoreen12
375b704eef fix: removed extra api call and strict mode (#1234)
* fix: removed extra api call and strict mode

* fix: added default values
2025-04-23 22:13:06 +05:00
Hassan Raza
ae121358db fix: Update ORA notification display title (#1233) 2025-04-23 16:09:29 +05:00
renovate[bot]
92b7c58af7 fix(deps): update dependency long to v5.3.2 (#1232)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-21 05:46:58 +00:00
renovate[bot]
a4097fe6fc fix(deps): update dependency @openedx/frontend-slot-footer to v1.2.1 (#1231)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-21 05:46:49 +00:00
Awais Ansari
397f688300 fix: scrolling issue for active menu item 2025-04-16 15:29:09 +05:00
Awais Ansari
8bd4b1b9a8 chore: updated package-lock file 2025-04-16 15:29:09 +05:00
Awais Ansari
54d029c181 test: updated test case snapshots 2025-04-16 15:29:09 +05:00
Hassan Raza
e6a4636147 fix: Update course preference title for ora submission (#1222) 2025-04-16 15:29:09 +05:00
sundasnoreen12
efb4162926 test: added test cases 2025-04-16 15:29:09 +05:00
sundasnoreen12
6061232e10 refactor: refactor code 2025-04-16 15:29:09 +05:00
sundasnoreen12
ba6b8c8f9b refactor: refactor code 2025-04-16 15:29:09 +05:00
sundasnoreen12
9c16ba0075 fix: fixed course level preference issue 2025-04-16 15:29:09 +05:00
sundasnoreen12
02b987909b fix: fixed varaible name 2025-04-16 15:29:09 +05:00
sundasnoreen12
1bcc54bb05 fix: added changes for restricted country 2025-04-16 15:29:09 +05:00
sundasnoreen12
5a5b0b905b fix: removed unused selector 2025-04-16 15:29:09 +05:00
sundasnoreen12
44ed49c7d2 refactor: refactored code 2025-04-16 15:29:09 +05:00
sundasnoreen12
386baa3840 fix: changed frequency from never to daily on email preference change 2025-04-16 15:29:09 +05:00
Awais Ansari
f1a56ad6bc fix: updated notifications section url (#1185)
* fix: updated notifiations section url

* fix: updated test cases
2025-04-16 15:29:09 +05:00
Awais Ansari
465bb9f7a0 feat: added notification preferences settings at account level (#1159)
* feat: added notification preferences settings at account level

* fix: fixed test cases

* feat: added api for account notification type

* fix: fixed test cases and label

* test: added update account preference test case

* fix: fixed issue to update email cadence for account notification type

* refactor: updated time

* fix: fixed mixed cadence issue

* fix: fixed border issue when no preferences

* refactor: refactor code

---------

Co-authored-by: sundasnoreen12 <sundasnoreen12@gmail.com>
2025-04-16 15:29:09 +05:00
Muhammad Adeel Tajamul
f9b7525d44 refactor: moved unable to delete into component (#1177) 2025-04-16 15:29:09 +05:00
Muhammad Adeel Tajamul
c7e82295c2 feat: added feature to hide delete button for countries (#1176) 2025-04-16 15:29:09 +05:00
sundasnoreen12
e02cf28b54 fix: rebase with 2u 2025-04-16 15:29:09 +05:00
Awais Ansari
18c51e8e73 fix: translation and console errors (#1166) 2025-04-16 15:29:09 +05:00
ayesha waris
88b444e796 chore: rebase with master (#1158)
* fix: fixed support urls (#1155)

* fix(deps): update dependency @edx/frontend-component-header to v5.7.1 (#1156)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix(deps): update dependency @openedx/frontend-slot-footer to v1.0.6 (#1157)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* fix: fixed certificates url

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-16 15:29:09 +05:00
Awais Ansari
71635b33b6 feat: added country disabling feature (#1116)
* feat: added country disabling feature

* refactor: removed isDisabledCountry additional call
2025-04-16 15:29:09 +05:00
Hunia Fatima
515890d5ef feat: upgrade react to v18 (#1220) 2025-04-04 11:29:11 -04:00
renovate[bot]
c2960e1232 chore(deps): update dependency @openedx/frontend-build to v14.4.1 (#1223)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-31 06:44:07 +00:00
Brian Smith
17e12f7f87 chore(deps): update @openedx dependencies to versions that support React 18 (#1218)
* chore(deps): update `@openedx` dependencies to versions that support React 18

* fix(tests): update `JumpNav` tests

* update JumpNav tests to use RTL render/screen instead of `react-test-renderer` snapshots
* update test names to reflect "optional information" not appearing in either scenario
  * the only difference in the snapshot ffc39868e9/src/account-settings/test/__snapshots__/JumpNav.test.jsx.snap is the delete account link

* chore(tests): update snapshots

* chore: fix lint
2025-03-25 12:22:13 -04:00
renovate[bot]
ffc39868e9 fix(deps): update react-router monorepo to v6.30.0 (#1217)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-24 07:32:44 +00:00
renovate[bot]
9ba01af816 fix(deps): update dependency qs to v6.14.0 (#1216)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-24 07:32:39 +00:00
Sarina Canelake
fc222cc76c Update README with Tutor instructions; update outdated edx.rtd.io links (#1203)
* docs: remove Devstack mentions from README instructions

* docs: Update edx.rtd.io links to point at new homes

* docs: Apply code review suggestions

Co-authored-by: Brian Smith <112954497+brian-smith-tcril@users.noreply.github.com>

---------

Co-authored-by: Brian Smith <112954497+brian-smith-tcril@users.noreply.github.com>
2025-03-21 15:15:04 +00:00
renovate[bot]
99a39568de fix(deps): update dependency @openedx/frontend-plugin-framework to v1.6.0 (#1210)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-17 06:23:54 +00:00
renovate[bot]
9ce3cbfddd fix(deps): update dependency long to v5.3.1 (#1211)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-17 06:23:47 +00:00
renovate[bot]
55a72b3f6e fix(deps): update dependency core-js to v3.41.0 (#1207)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-10 07:26:24 +00:00
renovate[bot]
597004c82e fix(deps): update dependency @openedx/frontend-slot-footer to v1.1.0 (#1206)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-10 07:25:41 +00:00
renovate[bot]
3458b6f410 chore(deps): update dependency @openedx/frontend-build to v14.3.2 (#1204)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-10 05:52:22 +00:00
renovate[bot]
b70b4a796f fix(deps): update dependency @openedx/frontend-plugin-framework to v1.5.0 (#1200)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-03 06:16:01 +00:00
renovate[bot]
d76945d2f2 fix(deps): update dependency @edx/frontend-platform to v8.2.1 (#1199)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-03 06:15:48 +00:00
renovate[bot]
7ee34f7d9a chore(deps): update dependency @openedx/frontend-build to v14.3.1 (#1196)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-24 06:00:19 +00:00
renovate[bot]
96c619cab8 fix(deps): update dependency @edx/frontend-component-header to v5.8.3 (#1195)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-24 06:00:08 +00:00
renovate[bot]
9eda5e588c chore(deps): update dependency @edx/browserslist-config to v1.5.0 (#1191)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-17 05:26:57 +00:00
Feanil Patel
3aff0e03e0 Merge pull request #1190 from openedx/feanil-patch-1
docs: Update catalog-info.yaml
2025-02-14 11:12:57 -05:00
Feanil Patel
852358f243 docs: Update catalog-info.yaml
This team was replace/renamed at some point but the catalog file was not updated.
2025-02-14 11:09:12 -05:00
Feanil Patel
5ffd17db4c Merge pull request #1186 from salman2013/salman/update-catalog-info-file
Update catalog-info-file for release data.
2025-01-24 09:40:22 -05:00
renovate[bot]
6f3c9616d6 fix(deps): update dependency long to v5.2.4 (#1188)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-20 05:37:47 +00:00
renovate[bot]
5f4812ed47 fix(deps): update dependency @edx/frontend-platform to v8.1.5 (#1187)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-20 05:33:41 +00:00
salman2013
d694b5c428 chore: update catalog-info-file for release data and remove openedx.yaml 2025-01-14 15:21:34 +05:00
renovate[bot]
6b73130e9b fix(deps): update font awesome to v6.7.2 (#1183)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-06 07:08:39 +00:00
renovate[bot]
d608f3947e fix(deps): update dependency @edx/frontend-platform to v8.1.4 (#1182)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-06 07:08:29 +00:00
renovate[bot]
db83838d5e fix(deps): update dependency @openedx/paragon to v22.13.0 (#1180)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-30 08:07:33 +00:00
renovate[bot]
004cdf35f1 fix(deps): update dependency core-js to v3.39.0 (#1181)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-30 08:07:20 +00:00
renovate[bot]
6cb015e49d chore(deps): update dependency @edx/browserslist-config to v1.4.0 (#1179)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-23 06:14:39 +00:00
renovate[bot]
c5ae2c40d7 fix(deps): update dependency @openedx/frontend-slot-footer to v1.0.7 (#1178)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-23 06:14:32 +00:00
renovate[bot]
46edd0cb70 fix(deps): update dependency @edx/frontend-platform to v8.1.3 (#1175)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-16 06:16:09 +00:00
renovate[bot]
2c6b5c34a9 fix(deps): update dependency @edx/frontend-component-header to v5.8.2 (#1174)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-16 06:15:57 +00:00
renovate[bot]
b000d2e048 fix(deps): update dependency @edx/frontend-component-header to v5.8.1 (#1173)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-09 05:35:05 +00:00
renovate[bot]
437fba16fe chore(deps): update dependency @openedx/frontend-build to v14.2.2 (#1172)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-09 05:34:44 +00:00
renovate[bot]
c4038b4085 fix(deps): update dependency @openedx/paragon to v22.10.0 (#1170)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-02 07:12:14 +00:00
renovate[bot]
496ff21015 fix(deps): update dependency @openedx/frontend-plugin-framework to v1.4.1 (#1169)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-02 07:11:53 +00:00
renovate[bot]
23e16ac6e0 fix(deps): update dependency @edx/frontend-component-header to v5.8.0 (#1163)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-27 09:42:18 +00:00
renovate[bot]
d8c5762ee2 chore(deps): update dependency @openedx/frontend-build to v14.2.0 (#1162)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-27 09:42:02 +00:00
sundasnoreen12
4d2d520234 Merge pull request #1165 from openedx/sundas/INF-1655
fix: fixed url break issue
2024-11-27 14:38:48 +05:00
sundasnoreen12
b094722772 fix: fixed url break issue 2024-11-26 17:01:27 +05:00
renovate[bot]
a9f061f10c fix(deps): update dependency qs to v6.13.1 (#1161)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-18 06:51:21 +00:00
renovate[bot]
4eb5d4effc fix(deps): update dependency @edx/frontend-component-header to v5.7.2 (#1160)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-18 06:49:32 +00:00
renovate[bot]
925bcce81c fix(deps): update dependency @openedx/frontend-slot-footer to v1.0.6 (#1157)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-11 07:12:06 +00:00
renovate[bot]
f44e03c7c8 fix(deps): update dependency @edx/frontend-component-header to v5.7.1 (#1156)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-11 07:11:39 +00:00
ayesha waris
999988dd88 fix: fixed support urls (#1155) 2024-11-08 14:12:27 +05:00
renovate[bot]
7ec1226965 fix(deps): update dependency @openedx/frontend-plugin-framework to v1.4.0 2024-11-04 06:24:25 +00:00
renovate[bot]
5b00275371 fix(deps): update dependency @edx/frontend-component-header to v5.7.0 2024-11-04 06:24:10 +00:00
renovate[bot]
30c1158775 fix(deps): update dependency universal-cookie to v7.2.2 2024-11-04 05:09:20 +00:00
renovate[bot]
c196b60b39 chore(deps): update dependency @testing-library/jest-dom to v6.6.3 2024-11-04 05:09:08 +00:00
Bilal Qamar
3022a267b1 test: Remove support for Node 18 (#1112) 2024-10-31 15:14:19 -04:00
renovate[bot]
19aacdfaf9 chore(deps): update dependency redux-mock-store to v1.5.5 2024-10-28 04:23:38 +00:00
renovate[bot]
530cca7c59 chore(deps): update dependency @edx/reactifex to v2 (#1148)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-24 17:11:49 +05:00
Bilal Qamar
e66d77cdd1 chore: update TensorFlow packages to v4.22.0 (#1147) 2024-10-24 16:30:42 +05:00
renovate[bot]
30333edfa2 fix(deps): update dependency universal-cookie to v7 (#1138)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-24 16:27:36 +05:00
Muhammad Adeel Tajamul
538d85b5dd feat: removed never from cadence options (#1142) 2024-10-23 13:28:58 +05:00
Brian Smith
8066ae58f1 feat(deps): update header to 5.6.0 (#1144) 2024-10-22 19:18:39 -04:00
Adolfo R. Brandes
8ce7362fd8 Merge pull request #1139 from eduNEXT/bav/fix-incorrect-post-accept
fix: send json accept headers for POST requests
2024-10-22 12:40:46 -03:00
Bilal Qamar
9c6f247f0a test: updated failing snapshots 2024-10-22 11:13:43 +00:00
renovate[bot]
085bc19313 fix(deps): update dependency @openedx/paragon to v22.9.0 2024-10-22 11:13:43 +00:00
Alison Langston
ee7c769c2f Merge pull request #1141 from openedx/alangsto/update_idv_failure_message
feat: remove learn more about IDV button
2024-10-21 15:10:15 -04:00
Alie Langston
a37be2dac1 feat: remove learn more about IDV button 2024-10-21 14:58:15 -04:00
renovate[bot]
d0fdc2ac5d chore(deps): update dependency @testing-library/jest-dom to v6.6.2 2024-10-21 07:10:59 +00:00
Bryann Valderrama
8a4eecf6c2 fix: add missing trailing comma 2024-10-16 10:33:37 -05:00
Maria Grimaldi
e2e4003b44 fix: send json accept headers for POST requests 2024-10-15 16:44:42 -05:00
renovate[bot]
434f114220 fix(deps): update react-router monorepo to v6.27.0 2024-10-14 05:00:08 +00:00
Eemaan Amir
c31e1648bf Merge pull request #1132 from openedx/INF-1653
chore: updated notificationTitle for oraGradeAssigned
2024-10-10 12:02:05 +05:00
eemaanamir
379d2df2fc chore: updated notificationTitle for oraGradeAssigned 2024-10-09 16:20:58 +05:00
renovate[bot]
5933ae3034 fix(deps): update dependency @edx/frontend-platform to v8.1.2 2024-10-07 06:48:36 +00:00
renovate[bot]
d1269197ac fix(deps): update dependency @edx/frontend-component-header to v5.5.0 2024-10-07 04:49:32 +00:00
renovate[bot]
12438e79f5 fix(deps): update dependency @openedx/frontend-plugin-framework to v1.3.0 2024-10-07 04:49:08 +00:00
renovate[bot]
57317a4bb6 chore(deps): update dependency @testing-library/jest-dom to v6.5.0 2024-09-30 09:57:32 +00:00
renovate[bot]
3bbc068a73 chore(deps): update dependency @openedx/frontend-build to v14.1.5 2024-09-30 07:44:46 +00:00
Justin Hynes
4179aa798a Merge pull request #1124 from openedx/jhynes/APER-3582_add-missing-setting-default
chore: add missing ACCOUNT_PROFILE_URL setting to the Account MFE
2024-09-24 11:43:27 -04:00
Justin Hynes
a1877bb499 chore: add missing ACCOUNT_PROFILE_URL setting to the Account MFE
This PR adds the `ACCOUNT_PROFILE_URL` setting to the `.env` and `.env.development` settings files
in order to make it easier to configure the communication between the Profile and Account MFEs in
Open edX development environments.
2024-09-24 15:14:29 +00:00
Eemaan Amir
cd44f15634 Merge pull request #1121 from openedx/INF-1580
chore: added notificationTitle for oraGradeAssigned
2024-09-23 18:05:53 +05:00
eemaanamir
83242b1042 chore: added notificationTitle for oraGradeAssigned 2024-09-23 16:57:01 +05:00
renovate[bot]
116c30663b fix(deps): update react-router monorepo to v6.26.2 2024-09-23 06:29:11 +00:00
renovate[bot]
0ed92cc7e8 chore(deps): update dependency @openedx/frontend-build to v14.1.4 2024-09-23 06:29:02 +00:00
renovate[bot]
3ec887089d fix(deps): update dependency core-js to v3.38.1 2024-09-16 04:23:45 +00:00
renovate[bot]
b46165ede0 fix(deps): update dependency @openedx/frontend-slot-footer to v1.0.5 2024-09-16 04:23:31 +00:00
renovate[bot]
752d760030 fix(deps): update dependency @openedx/frontend-slot-footer to v1.0.4 2024-09-09 06:50:34 +00:00
renovate[bot]
3e2c4dc760 chore(deps): update dependency @openedx/frontend-build to v14.1.2 2024-09-09 06:50:25 +00:00
renovate[bot]
4136a63e92 fix(deps): update dependency @edx/openedx-atlas to v0.6.2 2024-09-02 07:21:15 +00:00
renovate[bot]
ca8c9ef4fe chore(deps): update dependency @openedx/frontend-build to v14.1.1 2024-09-02 07:20:57 +00:00
Bilal Qamar
14f1cfd0e1 build: Upgrade to Node 20 (#1090)
* feat: updated node to v20

* refactor: updated package-lock

* refactor: updated package-lock

* refactor: updated lockfile version check workflow

* refactor: updated package-lock along with ci & lockfile version workflows

* refactor: updated lockfile version workflow
2024-08-29 12:08:39 -04:00
Marcos Rigoli
f81ec9544a Merge pull request #1108 from openedx/rijuma/verify-copy-change
fix: Updated copy change on Name Change modal
2024-08-27 10:22:28 -03:00
Marcos
11d7d4beb4 chore: Removed unused translation string 2024-08-23 09:21:45 -03:00
Bilal Qamar
70fc2f9642 test: Add Node 20 to CI matrix (#1107) 2024-08-22 14:35:46 -04:00
Marcos
f1d3c88f76 fix: Updated copy change on Name Change modal 2024-08-21 09:21:03 -03:00
renovate[bot]
c5c05c86f7 fix(deps): update react-router monorepo to v6.26.1 2024-08-19 05:33:50 +00:00
renovate[bot]
b845b34b02 chore(deps): update dependency @openedx/frontend-build to v14.1.0 2024-08-19 05:33:32 +00:00
Marcos Rigoli
27c666cc27 Merge pull request #1097 from openedx/rijuma/idv-experiment-slot
feat: Added PluginSlot to wrap IDVerificationPage component
2024-08-16 10:27:39 -03:00
Marcos
dcdd024809 docs: Updated documentation on the IDV Page Plugin Slot 2024-08-16 10:11:01 -03:00
Isaac Lee
dab4b1dc92 Merge pull request #1102 from openedx/ilee2u/local-module-development-instructions
docs: add local module dev info
2024-08-15 15:27:11 -04:00
ilee2u
c40bcd49ea docs: add local module dev info 2024-08-15 14:40:28 -04:00
renovate[bot]
8e3de876d0 fix(deps): update dependency qs to v6.13.0 2024-08-12 07:38:08 +00:00
renovate[bot]
aff083335c fix(deps): update dependency core-js to v3.38.0 2024-08-12 07:37:54 +00:00
renovate[bot]
18ea60b470 fix(deps): update dependency @openedx/frontend-plugin-framework to v1.2.3 2024-08-12 05:07:40 +00:00
renovate[bot]
9260b4834c chore(deps): update dependency @openedx/frontend-build to v14.0.15 2024-08-12 05:07:13 +00:00
Marcos
d7a5b222ee feat: Added PluginSlot to wrap IDVerificationPage component 2024-08-09 12:45:06 -03:00
Ahtisham Shahid
17ce32b80c fix: updated courseUpdate -> courseUpdates in title text (#1096) 2024-08-07 19:02:09 +05:00
sundasnoreen12
d4693a2c2e Merge pull request #1095 from openedx/sundas/INF-1482
docs: updated read me and catalog info files
2024-07-29 15:00:58 +05:00
sundasnoreen12
a446d5d3fc docs: updated read me and catalog info files 2024-07-29 11:38:07 +03:00
renovate[bot]
8f4139ad87 fix(deps): update dependency @openedx/frontend-slot-footer to v1.0.3 2024-07-29 06:25:22 +00:00
renovate[bot]
87e591cd71 fix(deps): update dependency @edx/frontend-platform to v8.1.1 2024-07-29 06:25:03 +00:00
renovate[bot]
90c02279e3 chore(deps): update dependency @testing-library/jest-dom to v6.4.8 2024-07-29 06:24:54 +00:00
renovate[bot]
600a3de7b6 chore(deps): update dependency @openedx/frontend-build to v14.0.14 2024-07-29 04:31:29 +00:00
Eemaan Amir
4390ca5435 Merge pull request #1089 from openedx/INF-1300
refactor: removed the channel wide toggle feature
2024-07-24 14:13:23 +05:00
eemaanamir
5903ad2ae8 refactor: removed the channel wide toggle feature 2024-07-23 16:32:48 +05:00
Hunia Fatima
cc50ad9744 chore(deps): update font awesome deps (#1087)
* chore(deps): update font awesome deps

* chore(deps): updated snapshots
2024-07-23 15:52:47 +05:00
Hunia Fatima
4e69301a4e chore(deps): update reselect (#1085) 2024-07-23 14:56:45 +05:00
Hunia Fatima
3a877eb55c chore(deps): update memoise one (#1083) 2024-07-23 14:56:34 +05:00
Hunia Fatima
673639caee chore(deps): update react router and react router dom (#1084) 2024-07-23 14:28:38 +05:00
Hunia Fatima
474f7c018d chore(deps): update qs (#1081) 2024-07-23 14:28:04 +05:00
Hunia Fatima
46398e7edc chore(deps): update frontend plugin framework (#1079) 2024-07-23 14:24:23 +05:00
renovate[bot]
dfb06268ab fix(deps): update dependency @edx/frontend-component-header to v5.3.4 2024-07-22 12:46:21 +00:00
Justin Hynes
b0301d5028 Merge pull request #1077 from openedx/jhynes/APER-2823_remove-2U-demographics-collection
feat!: remove 2U demographics collection
2024-07-11 12:54:30 -04:00
Justin Hynes
89d848e408 fix: quality 2024-07-11 13:34:45 +00:00
Justin Hynes
56ba73e455 feat!: remove 2U demographics collection
[APER-2823]

This PR removes functionality specific to 2U and edx.org from the Account MFE that was used for collecting demographics info from learners.
2024-07-11 13:27:05 +00:00
Muhammad Adeel Tajamul
c3d30227a3 temp: removing immediately from email preferences (#1076) 2024-07-08 13:25:50 +05:00
renovate[bot]
f5bedd7708 fix(deps): update react-router monorepo to v6.24.0 2024-07-01 04:53:19 +00:00
renovate[bot]
218c796500 fix(deps): update dependency @openedx/frontend-plugin-framework to v1.2.1 2024-07-01 04:53:01 +00:00
sundasnoreen12
604845f4a3 Merge pull request #1070 from openedx/sundass/INF-1415
fix: fix loading and margin issues
2024-06-24 13:41:09 +05:00
sundasnoreen12
751cdb1076 fix: fix loading and margin issues 2024-06-24 13:34:05 +05:00
renovate[bot]
0ebc4eb3cd fix(deps): update dependency form-urlencoded to v6.1.5 2024-06-24 07:05:41 +00:00
renovate[bot]
8a954c19e2 chore(deps): update dependency @openedx/frontend-build to v14.0.10 2024-06-24 07:05:25 +00:00
renovate[bot]
18b999b9a8 fix(deps): update dependency core-js to v3.37.1 2024-06-17 07:20:17 +00:00
renovate[bot]
b155d04814 chore(deps): update dependency @testing-library/jest-dom to v6.4.6 2024-06-17 07:20:03 +00:00
renovate[bot]
6f715e598f chore(deps): update dependency @openedx/frontend-build to v14.0.9 2024-06-17 04:06:17 +00:00
Adolfo R. Brandes
ab8d6e7913 build: Update codecov and use token
Update codecov to the latest version and start using the org-wide token for uploads.

See https://github.com/openedx/wg-frontend/issues/179
2024-06-14 14:59:37 -03:00
sundasnoreen12
d2bb164fab Merge pull request #1062 from openedx/sundas/INF-1415
feat: removed app level toggles
2024-06-14 03:07:36 -07:00
sundasnoreen12
5aacaf44a3 refactor: rename selector 2024-06-14 12:17:47 +05:00
sundasnoreen12
4502dbe413 fix: removed role button to remove cursor pointer 2024-06-12 13:59:24 +05:00
sundasnoreen12
f5e2fa7448 test: removed test cases 2024-06-12 13:53:34 +05:00
sundasnoreen12
5954b3122f fix: remove extra check 2024-06-12 00:59:39 -07:00
sundasnoreen12
2bf71cce10 feat: remove app level toggles 2024-06-12 00:57:27 -07:00
renovate[bot]
8fd0320c88 fix(deps): update dependency @fortawesome/react-fontawesome to v0.2.2 2024-06-10 06:16:54 +00:00
renovate[bot]
da15a70942 fix(deps): update dependency @edx/openedx-atlas to v0.6.1 2024-06-10 04:12:10 +00:00
edX requirements bot
52f4c51362 chore: update browserslist DB (#984)
Co-authored-by: abdullahwaheed <42172960+abdullahwaheed@users.noreply.github.com>
2024-06-05 11:36:51 +05:00
renovate[bot]
a0c2fb2686 fix(deps): update dependency @edx/frontend-platform to v8.0.4 2024-06-03 06:42:23 +00:00
renovate[bot]
223f792e2b fix(deps): update dependency @edx/frontend-component-header to v5.3.3 2024-06-03 04:10:15 +00:00
sundasnoreen12
175d9cf5cb Merge pull request #1054 from openedx/sundas/INF-1389
fix: disabled email cadence dropdown while api is pending
2024-05-23 14:17:48 +05:00
sundasnoreen12
94ef29938f fix: disabled email cadence dropdown while api is pending 2024-05-23 13:35:24 +05:00
renovate[bot]
4e38c9e9a6 fix(deps): update dependency @edx/frontend-platform to v8.0.3 2024-05-20 04:39:40 +00:00
Brian Smith
81a1ec7e1e fix: use frontend-slot-footer that supports all footer versions
The previous version of `frontend-slot-footer` had a peer dependency of `^14.0.0`, which caused problems for some methods of installing forked footers. This updates to a version of `frontend-slot-footer` that allows for *any* version of `frontend-component-footer` in the peer dependency.
2024-05-16 16:02:55 -03:00
sundasnoreen12
90da11782f Merge pull request #1049 from openedx/sundas/INF-1360
fix: fixed environment variable issue for email cadence
2024-05-16 13:32:47 +05:00
sundasnoreen12
82c9e07f0f refactor: optimize function for empty array 2024-05-16 12:53:36 +05:00
Brian Smith
16fb3a1bb4 fix: import FooterSlot from frontend-slot-footer package (#1048) 2024-05-15 16:46:34 -04:00
sundasnoreen12
e623f03c4b fix: hide type and web option from the header when there is no preference under app 2024-05-15 17:57:54 +05:00
sundasnoreen12
b2173afb9c refactor: removed lowercase function 2024-05-15 15:36:50 +05:00
sundasnoreen12
c787a77f9e refactor: fixed name 2024-05-15 15:11:15 +05:00
sundasnoreen12
075587c1f0 refactor: refactor name for notification channels 2024-05-15 14:20:13 +05:00
sundasnoreen12
a0b0b1f8d4 fix: fixed channels alignment issue 2024-05-15 14:13:00 +05:00
sundasnoreen12
16f20cad66 fix: fixed environment variable issue for email cadence 2024-05-15 13:21:58 +05:00
Muhammad Abdullah Waheed
40c67995a4 fix: reverted footer plugin to use simple footer (#1047) 2024-05-14 18:02:17 +05:00
sundasnoreen12
0fc68f7d93 Merge pull request #1043 from openedx/Ayesha/INF-1360-fix
fix: fixed hover color of cadence button
2024-05-14 16:12:49 +05:00
sundasnoreen12
90d3668128 fix: added paragon token for color 2024-05-14 16:09:40 +05:00
sundasnoreen12
1440c8239c Merge pull request #1046 from openedx/sundas/INF-1283
fix: core notification is now not visible under updates
2024-05-14 13:08:32 +05:00
sundasnoreen12
bb29f9624e fix: fixed test cases 2024-05-13 17:21:53 +05:00
sundasnoreen12
746b0fed4b fix: core notification is now not visible under updates 2024-05-13 16:03:13 +05:00
ayeshoali
02d14b95a7 fix: fixed hover color of cadence button 2024-05-13 14:58:14 +05:00
ayeshoali
484fc95ea0 refactor: added utils file for showEmailChannel 2024-05-13 13:36:08 +05:00
renovate[bot]
bdb851eb8b fix(deps): update dependency @edx/frontend-component-header to v5.3.1 2024-05-13 06:37:27 +00:00
ayeshoali
b984296550 refactor: grouped imports 2024-05-10 17:12:21 +05:00
ayeshoali
39fca96523 refactor: updated logic to handle show email channel env variable 2024-05-10 17:05:11 +05:00
Brian Smith
d1e817d4ba feat: use frontend-plugin-framework to provide a FooterSlot 2024-05-09 14:13:27 -03:00
ayeshoali
b35042ca97 refactor: resolved comments 2024-05-09 13:43:16 +05:00
ayeshoali
88e9eb3fdf refactor: added environment variable for email channel 2024-05-09 01:05:19 +05:00
ayeshoali
4409be4cc6 fix: fixed hover color of cadence button 2024-05-08 17:55:41 +05:00
147 changed files with 14140 additions and 11834 deletions

12
.env
View File

@@ -1,11 +1,10 @@
ACCESS_TOKEN_COOKIE_NAME=''
ACCOUNT_PROFILE_URL=''
BASE_URL=''
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=''
@@ -13,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=''
@@ -32,4 +33,7 @@ APP_ID=
MFE_CONFIG_API_URL=
PASSWORD_RESET_SUPPORT_LINK=''
LEARNER_FEEDBACK_URL=''
SUPPORT_URL_TO_UNLINK_SOCIAL_MEDIA_ACCOUNT='https://support.edx.org/hc/en-us/articles/207206067'
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={}

View File

@@ -1,11 +1,10 @@
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
ACCOUNT_PROFILE_URL='http://localhost:1995'
BASE_URL='localhost:1997'
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'
@@ -29,8 +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://support.edx.org/hc/en-us/articles/207206067'
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={}

View File

@@ -2,10 +2,8 @@ ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
BASE_URL='localhost:1997'
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'
@@ -26,9 +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://support.edx.org/hc/en-us/articles/207206067'
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={}

View File

@@ -3,3 +3,4 @@ dist/
node_modules/
__mocks__/
__snapshots__/
src/i18n/messages/

24
.github/pull_request_template.md vendored Normal file
View 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 **@jacobo-dominguez-wgu** to do it.
* [ ] 🎉 🙌 Celebrate! Thanks for your contribution.

View File

@@ -15,15 +15,15 @@ jobs:
- test
steps:
- uses: actions/checkout@v4
- name: Setup Nodejs Env
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
- uses: actions/setup-node@v2
- 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

View File

@@ -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

2
.nvmrc
View File

@@ -1 +1 @@
18
24

View File

@@ -1,5 +1,3 @@
intl_imports = ./node_modules/.bin/intl-imports.js
transifex_utils = ./node_modules/.bin/transifex-utils.js
i18n = ./src/i18n
@@ -19,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

View File

@@ -16,9 +16,6 @@ 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)
***************
@@ -28,40 +25,13 @@ Getting Started
Prerequisites
=============
The `devstack`_ is currently recommended as a development environment for your
new MFE. If you start it with ``make dev.up.lms`` that should give you
everything you need as a companion to this frontend.
Note that it is also possible to use `Tutor`_ to develop an MFE. You can refer
`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.
.. _Devstack: https://github.com/openedx/devstack
.. _Tutor: https://github.com/overhangio/tutor
.. _relevant tutor-mfe documentation: https://github.com/overhangio/tutor-mfe#mfe-development
Installation
============
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.
1. Install Devstack using the `Getting Started <https://github.com/openedx/devstack#getting-started>`_ instructions.
2. Start up Devstack, if it's not already started.
3. Log in to Devstack (http://localhost:18000/login )
4. Within this project, install requirements and start the development server:
.. 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
.. _relevant tutor-mfe documentation: https://github.com/overhangio/tutor-mfe?tab=readme-ov-file#mfe-development
Plugins
=======
@@ -72,7 +42,7 @@ The parts of this MFE that can be customized in that manner are documented `here
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:
@@ -99,31 +69,15 @@ 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:
``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
===================
@@ -135,9 +89,9 @@ Cloning and Startup
``git clone https://github.com/openedx/frontend-app-account.git``
2. Use node v18.x.
2. Use the version of Node specified in the ``.nvmrc`` file.
The current version of the micro-frontend build scripts support node 18.
The current version of the micro-frontend build scripts supports the version of Node found in ``.nvmrc``.
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>`_.
@@ -150,6 +104,40 @@ Cloning and Startup
``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
===========
@@ -160,7 +148,6 @@ 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
=======
@@ -216,6 +203,10 @@ 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
=========================
@@ -227,7 +218,7 @@ Please do not report security issues in public. Please email security@openedx.or
: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
View 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: jacobo-dominguez-wgu
type: 'website'
lifecycle: 'production'

15
codecov.yml Normal file
View 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"

View File

@@ -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}

15072
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,8 +10,10 @@
},
"scripts": {
"build": "fedx-scripts webpack",
"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"
@@ -28,25 +30,25 @@
],
"dependencies": {
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
"@edx/frontend-component-footer": "^13.2.0",
"@edx/frontend-component-header": "5.3.0",
"@edx/frontend-platform": "8.0.1",
"@edx/openedx-atlas": "^0.6.0",
"@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",
"@openedx/frontend-plugin-framework": "^1.1.2",
"@openedx/paragon": "22.0.0",
"@tensorflow-models/blazeface": "0.0.7",
"@tensorflow/tfjs-converter": "3.21.0",
"@tensorflow/tfjs-core": "3.21.0",
"bowser": "2.11.0",
"@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.14.1",
"classnames": "2.5.1",
"core-js": "3.37.0",
"core-js": "3.48.0",
"font-awesome": "4.7.0",
"form-urlencoded": "6.1.4",
"form-urlencoded": "6.1.6",
"formdata-polyfill": "4.0.10",
"jslib-html5-camera-photo": "3.3.4",
"lodash.camelcase": "4.3.0",
@@ -59,36 +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.12.1",
"react": "17.0.2",
"react-dom": "17.0.2",
"qs": "6.15.0",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-helmet": "6.1.0",
"react-redux": "7.2.9",
"react-router": "6.23.0",
"react-router-dom": "6.23.0",
"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.3.0",
"redux-saga": "1.4.2",
"redux-thunk": "2.4.2",
"regenerator-runtime": "0.14.1",
"reselect": "4.1.8",
"universal-cookie": "4.0.4"
"reselect": "^5.1.1",
"universal-cookie": "7.2.2"
},
"devDependencies": {
"@edx/browserslist-config": "1.2.0",
"@edx/reactifex": "1.1.0",
"@openedx/frontend-build": "14.0.3",
"@testing-library/jest-dom": "6.4.5",
"@testing-library/react": "12.1.5",
"react-test-renderer": "17.0.2",
"reactifex": "1.1.1",
"redux-mock-store": "1.5.4"
"@edx/browserslist-config": "1.5.1",
"@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"
}
}

View File

@@ -14,7 +14,7 @@ import {
getLanguageList,
} from '@edx/frontend-platform/i18n';
import {
Button, Hyperlink, Icon, Alert,
Container, Hyperlink, Icon, Alert,
} from '@openedx/paragon';
import { CheckCircle, Error, WarningFilled } from '@openedx/paragon/icons';
@@ -47,11 +47,13 @@ import {
COPPA_COMPLIANCE_YEAR,
WORK_EXPERIENCE_OPTIONS,
getStatesList,
FIELD_LABELS,
} from './data/constants';
import { fetchSiteLanguages } from './site-language';
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']),
@@ -149,11 +159,30 @@ class AccountSettingsPage extends React.Component {
})),
}));
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) => {
if (formId === FIELD_LABELS.COUNTRY && this.isDisabledCountry(values)) {
return;
}
const { formValues } = this.props;
let extendedProfileObject = {};
@@ -195,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);
}
@@ -313,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>
)
}
/>
@@ -460,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,
@@ -488,7 +503,8 @@ class AccountSettingsPage extends React.Component {
} = 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');
@@ -717,9 +733,10 @@ class AccountSettingsPage extends React.Component {
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.language.proficiencies.empty'])}
{...editableFieldProps}
/>
<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>
@@ -747,16 +764,19 @@ class AccountSettingsPage extends React.Component {
{...editableFieldProps}
/>
<EditableField
name="social_link_twitter"
name="social_link_x"
type="text"
value={this.props.formValues.social_link_twitter}
label={this.props.intl.formatMessage(messages['account.settings.field.social.platform.name.twitter'])}
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.social.platform.name.twitter.empty'])}
value={this.props.formValues.social_link_x}
label={this.props.intl.formatMessage(messages['account.settings.field.social.platform.name.xTwitter'])}
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.social.platform.name.xTwitter.empty'])}
{...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>
@@ -798,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>
)}
)}
</>
);
}
@@ -836,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>
);
}
}
@@ -874,21 +891,23 @@ 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.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,
social_link_linkedin: PropTypes.string,
social_link_facebook: PropTypes.string,
social_link_twitter: PropTypes.string,
social_link_x: PropTypes.string,
time_zone: PropTypes.string,
state: PropTypes.string,
shouldDisplayDemographicsSection: PropTypes.bool,
useVerifiedNameForCerts: PropTypes.bool.isRequired,
verified_name: PropTypes.string,
}).isRequired,
@@ -896,6 +915,7 @@ AccountSettingsPage.propTypes = {
name: PropTypes.string,
useVerifiedNameForCerts: PropTypes.bool,
verified_name: PropTypes.string,
country: PropTypes.string,
}),
drafts: PropTypes.shape({}),
formErrors: PropTypes.shape({
@@ -928,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,
@@ -954,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 = {
@@ -963,6 +992,7 @@ AccountSettingsPage.defaultProps = {
committedValues: {
useVerifiedNameForCerts: false,
verified_name: null,
country: '',
},
drafts: {},
formErrors: {},
@@ -975,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,

View File

@@ -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.',
@@ -519,15 +509,15 @@ const messages = defineMessages({
defaultMessage: 'Delete My Account',
description: 'Header for the user account deletion area',
},
'account.settings.field.social.platform.name.twitter': {
id: 'account.settings.field.social.platform.name.twitter',
defaultMessage: 'Twitter',
description: 'Label for Twitter',
'account.settings.field.social.platform.name.xTwitter': {
id: 'account.settings.field.social.platform.name.xTwitter',
defaultMessage: 'X (Twitter)',
description: 'Label for X (Twitter)',
},
'account.settings.field.social.platform.name.twitter.empty': {
id: 'account.settings.field.social.platform.name.twitter.empty',
defaultMessage: 'Add Twitter profile',
description: 'Placeholder for an empty Twitter field',
'account.settings.field.social.platform.name.xTwitter.empty': {
id: 'account.settings.field.social.platform.name.xTwitter.empty',
defaultMessage: 'Add X profile',
description: 'Placeholder for an empty X field',
},
'account.settings.field.social.platform.name.facebook': {

View File

@@ -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 '@openedx/paragon';
import React, { useCallback, useEffect, useState } from 'react';
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);

View File

@@ -1,8 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import classNames from 'classnames';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
Button, Form, StatefulButton,
} from '@openedx/paragon';
@@ -39,10 +38,10 @@ const EditableField = (props) => {
isEditing,
isEditable,
isGrayedOut,
intl,
...others
} = props;
const id = `field-${name}`;
const intl = useIntl();
const handleSubmit = (e) => {
e.preventDefault();
@@ -85,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 (
@@ -96,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}
@@ -109,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>
@@ -134,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: (
@@ -151,7 +160,7 @@ 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}
@@ -188,7 +197,6 @@ EditableField.propTypes = {
isEditing: PropTypes.bool,
isEditable: PropTypes.bool,
isGrayedOut: PropTypes.bool,
intl: intlShape.isRequired,
};
EditableField.defaultProps = {
@@ -209,4 +217,4 @@ EditableField.defaultProps = {
export default connect(editableFieldSelector, {
onEdit: openForm,
onCancel: closeForm,
})(injectIntl(EditableField));
})(EditableField);

View File

@@ -1,7 +1,6 @@
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 '@openedx/paragon';
@@ -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}`;
@@ -107,6 +106,7 @@ const EditableSelectField = (props) => {
<option
value={subOption.value}
key={`${subOption.value}-${subOption.label}`}
disabled={subOption?.disabled}
>
{subOption.label}
</option>
@@ -115,7 +115,7 @@ const EditableSelectField = (props) => {
);
}
return (
<option value={option.value} key={`${option.value}-${option.label}`}>
<option value={option.value} key={`${option.value}-${option.label}`} disabled={option?.disabled}>
{option.label}
</option>
);
@@ -226,7 +226,6 @@ EditableSelectField.propTypes = {
isEditing: PropTypes.bool,
isEditable: PropTypes.bool,
isGrayedOut: PropTypes.bool,
intl: intlShape.isRequired,
};
EditableSelectField.defaultProps = {
@@ -248,4 +247,4 @@ EditableSelectField.defaultProps = {
export default connect(editableFieldSelector, {
onEdit: openForm,
onCancel: closeForm,
})(injectIntl(EditableSelectField));
})(EditableSelectField);

View File

@@ -1,9 +1,8 @@
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,
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);

View File

@@ -1,38 +1,30 @@
import { getConfig } from '@edx/frontend-platform';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { breakpoints, useWindowSize, Icon } from '@openedx/paragon';
import { OpenInNew } from '@openedx/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;

View File

@@ -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;
}

View File

@@ -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';
@@ -8,7 +8,7 @@ import {
ModalDialog,
StatefulButton,
} from '@openedx/paragon';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
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);

View File

@@ -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);
});
});

View File

@@ -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);
@@ -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);

View File

@@ -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,
},
});

View File

@@ -132,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',
};

View File

@@ -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 {

View File

@@ -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));

View File

@@ -88,6 +88,11 @@ const previousSiteLanguageSelector = createSelector(
accountSettings => accountSettings.previousSiteLanguage,
);
const countriesSelector = createSelector(
accountSettingsSelector,
accountSettings => accountSettings.countriesCodesList,
);
const editableFieldErrorSelector = createSelector(
editableFieldNameSelector,
accountSettingsSelector,
@@ -237,6 +242,7 @@ export const accountSettingsPageSelector = createSelector(
mostRecentApprovedVerifiedNameValueSelector,
mostRecentVerifiedNameSelector,
sortedVerifiedNameHistorySelector,
countriesSelector,
(
accountSettings,
siteLanguageOptions,
@@ -254,6 +260,7 @@ export const accountSettingsPageSelector = createSelector(
verifiedName,
mostRecentVerifiedName,
verifiedNameHistory,
countriesCodesList,
) => ({
siteLanguageOptions,
siteLanguage,
@@ -274,6 +281,7 @@ export const accountSettingsPageSelector = createSelector(
verifiedName,
mostRecentVerifiedName,
verifiedNameHistory,
countriesCodesList,
}),
);
@@ -298,21 +306,6 @@ export const certPreferenceSelector = createSelector(
}),
);
export const demographicsSectionSelector = createSelector(
formValuesSelector,
draftsSelector,
errorSelector,
(
formValues,
drafts,
errors,
) => ({
formValues,
drafts,
formErrors: errors,
}),
);
export const nameChangeSelector = createSelector(
accountSettingsSelector,
formValuesSelector,

View File

@@ -1,18 +1,17 @@
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 { 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' },
{ id: 'xTwitter', key: 'social_link_x' },
{ id: 'facebook', key: 'social_link_facebook' },
{ id: 'linkedin', key: 'social_link_linkedin' },
];
@@ -154,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();
@@ -211,29 +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, 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,
shouldDisplayDemographicsQuestionsResponse,
demographics,
demographicsOptions,
countries,
] = await Promise.all([
getAccount(username),
getPreferences(username),
getThirdPartyAuthProviders(),
getProfileDataManager(username, userRoles),
getTimeZones(),
getConfig().ENABLE_DEMOGRAPHICS_COLLECTION && shouldDisplayDemographicsQuestions(),
getConfig().ENABLE_DEMOGRAPHICS_COLLECTION && getDemographics(userId),
getConfig().ENABLE_DEMOGRAPHICS_COLLECTION && getDemographicsOptions(),
getCountryList(),
]);
return {
@@ -242,9 +233,7 @@ export async function getSettings(username, userRoles, userId) {
thirdPartyAuthProviders,
profileDataManager,
timeZones,
shouldDisplayDemographicsSection: shouldDisplayDemographicsQuestionsResponse,
...demographics,
demographicsOptions,
countries,
};
}
@@ -252,22 +241,18 @@ export async function getSettings(username, userRoles, userId) {
* A single function to PATCH everything considered a setting.
* 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 demographicsKeys = DEMOGRAPHICS_FIELDS;
const certificateKeys = ['useVerifiedNameForCerts'];
const isDemographicsKey = (value, key) => key.includes('demographics');
const accountCommitValues = omit(
commitValues,
preferenceKeys,
demographicsKeys,
certificateKeys,
);
const preferenceCommitValues = pick(commitValues, preferenceKeys);
const demographicsCommitValues = pickBy(commitValues, isDemographicsKey);
const certCommitValues = pick(commitValues, certificateKeys);
const patchRequests = [];
@@ -277,9 +262,6 @@ export async function patchSettings(username, commitValues, userId) {
if (!isEmpty(preferenceCommitValues)) {
patchRequests.push(patchPreferences(username, preferenceCommitValues));
}
if (!isEmpty(demographicsCommitValues)) {
patchRequests.push(patchDemographics(userId, demographicsCommitValues));
}
if (!isEmpty(certCommitValues)) {
patchRequests.push(postVerifiedNameConfig(username, certCommitValues));
}

View 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: 'xTwitter', 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_x).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_x: 'http://t' };
const apiResponse = {
username: 'testuser',
social_links: [{ platform: 'xTwitter', 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: 'xTwitter', social_link: 'http://t' }] }),
expect.any(Object),
);
expect(result.social_link_x).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');
});
});
});

View File

@@ -1,6 +1,5 @@
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 '@openedx/paragon';
@@ -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
@@ -41,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;

View File

@@ -1,25 +1,23 @@
import React from 'react';
import ReactDOM from 'react-dom';
import renderer from 'react-test-renderer';
import { IntlProvider, injectIntl, createIntl } from '@edx/frontend-platform/i18n';
import { IntlProvider } from '@edx/frontend-platform/i18n';
ReactDOM.createPortal = node => node;
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
const IntlBeforeProceedingBanner = injectIntl(BeforeProceedingBanner);
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',
intl: createIntl({ locale: 'en' }),
supportArticleUrl: '',
};
const tree = renderer
.create((
<IntlProvider locale="en">
<IntlBeforeProceedingBanner
<BeforeProceedingBanner
{...props}
/>
</IntlProvider>
@@ -31,13 +29,12 @@ describe('BeforeProceedingBanner', () => {
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',
intl: createIntl({ locale: 'en' }),
supportArticleUrl: 'http://test-support.edx',
};
const tree = renderer
.create((
<IntlProvider locale="en">
<IntlBeforeProceedingBanner
<BeforeProceedingBanner
{...props}
/>
</IntlProvider>

View File

@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import {
AlertModal,
Button, Input, ValidationFormGroup, ActionRow,
Button, Form, ActionRow,
} from '@openedx/paragon';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { faExclamationCircle, faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
@@ -78,10 +78,11 @@ 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>
<Button variant="danger" onClick={onSubmit}>Yes, Delete</Button>
<Button variant="link" onClick={onCancel}>{intl.formatMessage(messages['account.settings.delete.account.modal.confirm.cancel'])}</Button>
<Button variant="danger" onClick={onSubmit}>{intl.formatMessage(messages['account.settings.delete.account.modal.confirm.delete'])}</Button>
</ActionRow>
)}
>
@@ -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>

View File

@@ -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

View File

@@ -76,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={supportArticleUrl}
/>
) : 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>
);
}
@@ -152,6 +159,7 @@ DeleteAccount.propTypes = {
errorType: PropTypes.oneOf(['empty-password', 'server']),
hasLinkedTPA: PropTypes.bool,
isVerifiedAccount: PropTypes.bool,
canDeleteAccount: PropTypes.bool,
intl: intlShape.isRequired,
};
@@ -160,6 +168,7 @@ DeleteAccount.defaultProps = {
isVerifiedAccount: true,
status: null,
errorType: null,
canDeleteAccount: true,
};
// Assume we're part of the accountSettings state.

View File

@@ -1,19 +1,19 @@
import React from 'react';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
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;

View File

@@ -1,12 +1,12 @@
import React from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
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;

View File

@@ -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();
});
});
});

View File

@@ -7,17 +7,17 @@ exports[`BeforeProceedingBanner should match the snapshot if SUPPORT_URL_TO_UNLI
<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={{}}
viewBox="0 0 576 512"
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={{}}
/>
@@ -36,17 +36,17 @@ exports[`BeforeProceedingBanner should match the snapshot when SUPPORT_URL_TO_UN
<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={{}}
viewBox="0 0 576 512"
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={{}}
/>
@@ -57,7 +57,6 @@ exports[`BeforeProceedingBanner should match the snapshot when SUPPORT_URL_TO_UN
<a
className="pgn__hyperlink default-link standalone-link"
href="http://test-support.edx"
onClick={[Function]}
target="_self"
>
unlink all social media accounts

View File

@@ -38,10 +38,11 @@ exports[`ConfirmationModal should match empty password confirmation modal snapsh
data-testid="modal-backdrop"
onClick={[MockFunction]}
onKeyDown={[MockFunction]}
role="presentation"
/>
<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
@@ -69,8 +70,8 @@ exports[`ConfirmationModal should match empty password confirmation modal snapsh
<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"
@@ -79,7 +80,7 @@ exports[`ConfirmationModal should match empty password confirmation modal snapsh
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={{}}
/>
@@ -102,17 +103,17 @@ exports[`ConfirmationModal should match empty password confirmation modal snapsh
<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={{}}
viewBox="0 0 576 512"
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={{}}
/>
@@ -131,30 +132,57 @@ exports[`ConfirmationModal should match empty password confirmation modal snapsh
</div>
</div>
<div
className="form-group"
data-testid="validation-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>
@@ -220,15 +248,17 @@ exports[`ConfirmationModal should match open confirmation modal snapshot 1`] = `
"width": "1px",
}
}
tabIndex={-1}
tabIndex={0}
/>,
<div
className="pgn__modal-layer"
data-focus-lock-disabled="disabled"
data-focus-lock-disabled={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onScrollCapture={[Function]}
onTouchMoveCapture={[Function]}
onTouchStart={[Function]}
onWheelCapture={[Function]}
>
<div
@@ -239,10 +269,11 @@ exports[`ConfirmationModal should match open confirmation modal snapshot 1`] = `
data-testid="modal-backdrop"
onClick={[MockFunction]}
onKeyDown={[MockFunction]}
role="presentation"
/>
<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
@@ -270,17 +301,17 @@ exports[`ConfirmationModal should match open confirmation modal 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={{}}
viewBox="0 0 576 512"
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={{}}
/>
@@ -299,30 +330,28 @@ exports[`ConfirmationModal should match open confirmation modal snapshot 1`] = `
</div>
</div>
<div
className="form-group"
data-testid="validation-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>
@@ -368,7 +397,7 @@ exports[`ConfirmationModal should match open confirmation modal snapshot 1`] = `
"width": "1px",
}
}
tabIndex={-1}
tabIndex={0}
/>,
]
`;

View File

@@ -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,17 +95,17 @@ 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={{}}
viewBox="0 0 576 512"
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={{}}
/>
@@ -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,17 +175,17 @@ 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={{}}
viewBox="0 0 576 512"
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={{}}
/>
@@ -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

View File

@@ -23,15 +23,17 @@ exports[`SuccessModal should match open success modal snapshot 1`] = `
"width": "1px",
}
}
tabIndex={-1}
tabIndex={0}
/>,
<div
className="pgn__modal-layer"
data-focus-lock-disabled="disabled"
data-focus-lock-disabled={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onScrollCapture={[Function]}
onTouchMoveCapture={[Function]}
onTouchStart={[Function]}
onWheelCapture={[Function]}
>
<div
@@ -42,6 +44,7 @@ exports[`SuccessModal should match open success modal snapshot 1`] = `
data-testid="modal-backdrop"
onClick={[MockFunction]}
onKeyDown={[MockFunction]}
role="presentation"
/>
<div
className="mw-sm p-5 bg-white mx-auto my-3"
@@ -84,7 +87,7 @@ exports[`SuccessModal should match open success modal snapshot 1`] = `
"width": "1px",
}
}
tabIndex={-1}
tabIndex={0}
/>,
]
`;

View 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);
});
});

View File

@@ -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',

View File

@@ -1,80 +0,0 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { Form } from '@openedx/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;

View File

@@ -1,357 +0,0 @@
import { getConfig } from '@edx/frontend-platform';
import {
FormattedMessage,
injectIntl,
intlShape,
} from '@edx/frontend-platform/i18n';
import { Hyperlink, Form } from '@openedx/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));

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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();
});
});

View File

@@ -1,3953 +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"
data-testid="hyperlink-icon"
style={
{
"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={
{
"height": null,
}
}
>
<div
style={
{
"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={{}}
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={{}}
/>
</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={
{
"height": null,
}
}
>
<div
style={
{
"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={{}}
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={{}}
/>
</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={
{
"height": null,
}
}
>
<div
style={
{
"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={{}}
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={{}}
/>
</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={
{
"height": null,
}
}
>
<div
style={
{
"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={{}}
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={{}}
/>
</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={
{
"height": null,
}
}
>
<div
style={
{
"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={{}}
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={{}}
/>
</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={
{
"height": null,
}
}
>
<div
style={
{
"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={{}}
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={{}}
/>
</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={
{
"height": null,
}
}
>
<div
style={
{
"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={{}}
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={{}}
/>
</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={
{
"height": null,
}
}
>
<div
style={
{
"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={{}}
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={{}}
/>
</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={
{
"height": null,
}
}
>
<div
style={
{
"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={{}}
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={{}}
/>
</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"
data-testid="hyperlink-icon"
style={
{
"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={
{
"height": null,
}
}
>
<div
style={
{
"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={{}}
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={{}}
/>
</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={
{
"height": null,
}
}
>
<div
style={
{
"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={{}}
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={{}}
/>
</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={
{
"height": null,
}
}
>
<div
style={
{
"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={{}}
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={{}}
/>
</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={
{
"height": null,
}
}
>
<div
style={
{
"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={{}}
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={{}}
/>
</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={
{
"height": null,
}
}
>
<div
style={
{
"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={{}}
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={{}}
/>
</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={
{
"height": null,
}
}
>
<div
style={
{
"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={{}}
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={{}}
/>
</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={
{
"height": null,
}
}
>
<div
style={
{
"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={{}}
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={{}}
/>
</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={
{
"height": null,
}
}
>
<div
style={
{
"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={{}}
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={{}}
/>
</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={
{
"height": null,
}
}
>
<div
style={
{
"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={{}}
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={{}}
/>
</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"
data-testid="hyperlink-icon"
style={
{
"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"
data-testid="hyperlink-icon"
style={
{
"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={
{
"height": null,
}
}
>
<div
style={
{
"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={{}}
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={{}}
/>
</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={
{
"height": null,
}
}
>
<div
style={
{
"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={{}}
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={{}}
/>
</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={
{
"height": null,
}
}
>
<div
style={
{
"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={{}}
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={{}}
/>
</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={
{
"height": null,
}
}
>
<div
style={
{
"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={{}}
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={{}}
/>
</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={
{
"height": null,
}
}
>
<div
style={
{
"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={{}}
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={{}}
/>
</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={
{
"height": null,
}
}
>
<div
style={
{
"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={{}}
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={{}}
/>
</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={
{
"height": null,
}
}
>
<div
style={
{
"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={{}}
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={{}}
/>
</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={
{
"height": null,
}
}
>
<div
style={
{
"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={{}}
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={{}}
/>
</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={
{
"height": null,
}
}
>
<div
style={
{
"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={{}}
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={{}}
/>
</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"
data-testid="hyperlink-icon"
style={
{
"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={
{
"height": null,
}
}
>
<div
style={
{
"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={{}}
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={{}}
/>
</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={
{
"height": null,
}
}
>
<div
style={
{
"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={{}}
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={{}}
/>
</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={
{
"height": null,
}
}
>
<div
style={
{
"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={{}}
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={{}}
/>
</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={
{
"height": null,
}
}
>
<div
style={
{
"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={{}}
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={{}}
/>
</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={
{
"height": null,
}
}
>
<div
style={
{
"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={{}}
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={{}}
/>
</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={
{
"height": null,
}
}
>
<div
style={
{
"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={{}}
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={{}}
/>
</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={
{
"height": null,
}
}
>
<div
style={
{
"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={{}}
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={{}}
/>
</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={
{
"height": null,
}
}
>
<div
style={
{
"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={{}}
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={{}}
/>
</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={
{
"height": null,
}
}
>
<div
style={
{
"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={{}}
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={{}}
/>
</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"
data-testid="hyperlink-icon"
style={
{
"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={
{
"height": null,
}
}
>
<div
style={
{
"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={{}}
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={{}}
/>
</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={
{
"height": null,
}
}
>
<div
style={
{
"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={{}}
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={{}}
/>
</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={
{
"height": null,
}
}
>
<div
style={
{
"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={{}}
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={{}}
/>
</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={
{
"height": null,
}
}
>
<div
style={
{
"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={{}}
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={{}}
/>
</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={
{
"height": null,
}
}
>
<div
style={
{
"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={{}}
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={{}}
/>
</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={
{
"height": null,
}
}
>
<div
style={
{
"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={{}}
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={{}}
/>
</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={
{
"height": null,
}
}
>
<div
style={
{
"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={{}}
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={{}}
/>
</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={
{
"height": null,
}
}
>
<div
style={
{
"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={{}}
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={{}}
/>
</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={
{
"height": null,
}
}
>
<div
style={
{
"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={{}}
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={{}}
/>
</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"
data-testid="hyperlink-icon"
style={
{
"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={
{
"height": null,
}
}
>
<div
style={
{
"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={{}}
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={{}}
/>
</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={
{
"height": null,
}
}
>
<div
style={
{
"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={{}}
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={{}}
/>
</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={
{
"height": null,
}
}
>
<div
style={
{
"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={{}}
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={{}}
/>
</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={
{
"height": null,
}
}
>
<div
style={
{
"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={{}}
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={{}}
/>
</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={
{
"height": null,
}
}
>
<div
style={
{
"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={{}}
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={{}}
/>
</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={
{
"height": null,
}
}
>
<div
style={
{
"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={{}}
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={{}}
/>
</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={
{
"height": null,
}
}
>
<div
style={
{
"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={{}}
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={{}}
/>
</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={
{
"height": null,
}
}
>
<div
style={
{
"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={{}}
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={{}}
/>
</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={
{
"height": null,
}
}
>
<div
style={
{
"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={{}}
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={{}}
/>
</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>
`;

View File

@@ -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,
@@ -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);

View 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);
});
});

View File

@@ -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': {

View File

@@ -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');
});
});

View File

@@ -1,7 +1,6 @@
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 { StatefulButton } from '@openedx/paragon';
import { resetPassword } from './data/actions';
@@ -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);

View 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);
});
});

View File

@@ -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);
}

View 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();
});
});
});

View File

@@ -11,11 +11,12 @@ import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
import AccountSettingsPage from '../AccountSettingsPage';
import mockData from './mockData';
import messages from '../AccountSettingsPage.messages';
const mockDispatch = jest.fn();
jest.mock('@edx/frontend-platform/analytics', () => ({
sendTrackingLogEvent: jest.fn(),
getCountryList: jest.fn(),
getCountryList: jest.fn(() => [{ code: 'US', name: 'United States' }]),
}));
jest.mock('react-redux', () => ({
@@ -25,6 +26,19 @@ jest.mock('react-redux', () => ({
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];
@@ -63,14 +77,67 @@ describe('AccountSettingsPage', () => {
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} />));
@@ -98,4 +165,70 @@ describe('AccountSettingsPage', () => {
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(messages['account.settings.field.social.platform.name.xTwitter.empty'].defaultMessage)).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();
});
});

View 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 });
});
});

View 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();
});
});

View File

@@ -1,10 +1,9 @@
import React from 'react';
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, injectIntl } from '@edx/frontend-platform/i18n';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import EditableSelectField from '../EditableSelectField';
@@ -17,8 +16,6 @@ jest.mock('react-redux', () => ({
jest.mock('@edx/frontend-platform/auth');
jest.mock('../data/selectors', () => jest.fn().mockImplementation(() => ({ certPreferenceSelector: () => ({}) })));
const IntlEditableSelectField = injectIntl(EditableSelectField);
const mockStore = configureStore();
describe('EditableSelectField', () => {
@@ -88,7 +85,7 @@ describe('EditableSelectField', () => {
afterEach(() => jest.clearAllMocks());
it('renders EditableSelectField correctly with editing disabled', () => {
const tree = renderer.create(reduxWrapper(<IntlEditableSelectField {...props} />)).toJSON();
const tree = renderer.create(reduxWrapper(<EditableSelectField {...props} />)).toJSON();
expect(tree).toMatchSnapshot();
});
@@ -98,7 +95,7 @@ describe('EditableSelectField', () => {
isEditing: true,
};
const tree = renderer.create(reduxWrapper(<IntlEditableSelectField {...props} />)).toJSON();
const tree = renderer.create(reduxWrapper(<EditableSelectField {...props} />)).toJSON();
expect(tree).toMatchSnapshot();
});
@@ -107,7 +104,7 @@ describe('EditableSelectField', () => {
...props,
error: 'This is an error message',
};
const tree = renderer.create(reduxWrapper(<IntlEditableSelectField {...errorProps} />)).toJSON();
const tree = renderer.create(reduxWrapper(<EditableSelectField {...errorProps} />)).toJSON();
expect(tree).toMatchSnapshot();
});
@@ -126,7 +123,7 @@ describe('EditableSelectField', () => {
},
],
};
const tree = renderer.create(reduxWrapper(<IntlEditableSelectField {...propsWithGroup} />)).toJSON();
const tree = renderer.create(reduxWrapper(<EditableSelectField {...propsWithGroup} />)).toJSON();
expect(tree).toMatchSnapshot();
});
@@ -140,7 +137,7 @@ describe('EditableSelectField', () => {
},
],
};
const tree = renderer.create(reduxWrapper(<IntlEditableSelectField {...propsWithoutGroup} />)).toJSON();
const tree = renderer.create(reduxWrapper(<EditableSelectField {...propsWithoutGroup} />)).toJSON();
expect(tree).toMatchSnapshot();
});
@@ -163,7 +160,7 @@ describe('EditableSelectField', () => {
},
],
};
const tree = renderer.create(reduxWrapper(<IntlEditableSelectField {...propsWithGroups} />)).toJSON();
const tree = renderer.create(reduxWrapper(<EditableSelectField {...propsWithGroups} />)).toJSON();
expect(tree).toMatchSnapshot();
});
});

View File

@@ -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();
});
});

View File

@@ -35,8 +35,8 @@ exports[`EditableSelectField renders EditableSelectField correctly with editing
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
data-icon="pencil-alt"
className="svg-inline--fa fa-pencil mr-1"
data-icon="pencil"
data-prefix="fas"
focusable="false"
role="img"
@@ -45,7 +45,7 @@ exports[`EditableSelectField renders EditableSelectField correctly with editing
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"
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={{}}
/>
@@ -172,9 +172,7 @@ exports[`EditableSelectField renders EditableSelectField correctly with editing
/>
</svg>
</span>
<div>
</div>
<div />
</div>
</div>
<p>
@@ -243,8 +241,8 @@ exports[`EditableSelectField renders EditableSelectField with an error 1`] = `
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
data-icon="pencil-alt"
className="svg-inline--fa fa-pencil mr-1"
data-icon="pencil"
data-prefix="fas"
focusable="false"
role="img"
@@ -253,7 +251,7 @@ exports[`EditableSelectField renders EditableSelectField with an error 1`] = `
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"
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={{}}
/>
@@ -312,8 +310,8 @@ exports[`EditableSelectField renders selectOptions when option does not have a g
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
data-icon="pencil-alt"
className="svg-inline--fa fa-pencil mr-1"
data-icon="pencil"
data-prefix="fas"
focusable="false"
role="img"
@@ -322,7 +320,7 @@ exports[`EditableSelectField renders selectOptions when option does not have a g
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"
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={{}}
/>
@@ -381,8 +379,8 @@ exports[`EditableSelectField renders selectOptions when option has a group 1`] =
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
data-icon="pencil-alt"
className="svg-inline--fa fa-pencil mr-1"
data-icon="pencil"
data-prefix="fas"
focusable="false"
role="img"
@@ -391,7 +389,7 @@ exports[`EditableSelectField renders selectOptions when option has a group 1`] =
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"
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={{}}
/>
@@ -450,8 +448,8 @@ exports[`EditableSelectField renders selectOptions with multiple groups 1`] = `
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
data-icon="pencil-alt"
className="svg-inline--fa fa-pencil mr-1"
data-icon="pencil"
data-prefix="fas"
focusable="false"
role="img"
@@ -460,7 +458,7 @@ exports[`EditableSelectField renders selectOptions with multiple groups 1`] = `
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"
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={{}}
/>

View File

@@ -1,197 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`JumpNav should not render Optional Information or delete account link 1`] = `
<div
data-testid="redux-provider"
>
<div
data-testid="browser-router"
>
<div
className="jump-nav px-2.25 jump-nav-sm position-sticky pt-3"
>
<ul
className="list-unstyled"
style={{}}
>
<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>
</div>
</div>
`;
exports[`JumpNav should render Optional Information and delete account link 1`] = `
<div
data-testid="redux-provider"
>
<div
data-testid="browser-router"
>
<div
className="jump-nav px-2.25 jump-nav-sm position-sticky pt-3"
>
<ul
className="list-unstyled"
style={{}}
>
<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>
</div>
</div>
`;

View File

@@ -6,7 +6,7 @@ const mockData = {
data: null,
values: {
username: 'test_username',
country: 'AD',
country: 'US',
accomplishments_shared: false,
name: 'test_name',
email: 'test_email@test.com',
@@ -18,11 +18,18 @@ const mockData = {
field_value: '',
},
],
gender: null,
gender: 'm',
'pref-lang': 'en',
shouldDisplayDemographicsSection: false,
demographicsOptions: false,
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: {},
@@ -30,14 +37,14 @@ const mockData = {
saveState: null,
timeZones: [
{
time_zone: 'Africa/Abidjan',
description: 'Africa/Abidjan (GMT, UTC+0000)',
time_zone: 'America/New_York',
description: 'America/New_York (EST, UTC-0500)',
},
],
countryTimeZones: [
{
time_zone: 'Europe/Andorra',
description: 'Europe/Andorra (CET, UTC+0100)',
time_zone: 'America/New_York',
description: 'America/New_York (EST, UTC-0500)',
},
],
previousSiteLanguage: null,
@@ -87,7 +94,7 @@ const mockData = {
profileDataManager: null,
},
notificationPreferences: {
showPreferences: false,
showPreferences: true,
courses: {
status: 'success',
courses: [],
@@ -101,7 +108,7 @@ const mockData = {
preferences: {
status: 'idle',
updatePreferenceStatus: 'idle',
selectedCourse: null,
selectedCourse: 'account',
preferences: [],
apps: [],
nonEditable: {},

View File

@@ -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
View 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
View File

@@ -0,0 +1,2 @@
// eslint-disable-next-line import/prefer-default-export
export { default as Divider } from './Divider';

View File

@@ -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;

View File

@@ -6,9 +6,8 @@ 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', () => {
render(<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');

View File

@@ -1,4 +1,5 @@
import { useEffect, useState } from 'react';
import { getConfig } from '@edx/frontend-platform';
import { logError } from '@edx/frontend-platform/logging';
import { breakpoints, useWindowSize } from '@openedx/paragon';
@@ -70,5 +71,5 @@ export function useFeedbackWrapper() {
export function useIsOnMobile() {
const windowSize = useWindowSize();
return windowSize.width <= breakpoints.small.minWidth;
return windowSize.width <= breakpoints.small.maxWidth;
}

View File

@@ -1,12 +1,13 @@
import React from 'react';
import PropTypes from 'prop-types';
import { getConfig } from '@edx/frontend-platform';
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
import messages from './IdVerification.messages';
import { ERROR_REASONS } from './IdVerificationContext';
const AccessBlocked = ({ error, intl }) => {
const AccessBlocked = ({ error }) => {
const intl = useIntl();
const handleMessage = () => {
if (error === ERROR_REASONS.COURSE_ENROLLMENT) {
return <p>{intl.formatMessage(messages['id.verification.access.blocked.enrollment'])}</p>;
@@ -42,8 +43,7 @@ const AccessBlocked = ({ error, intl }) => {
};
AccessBlocked.propTypes = {
intl: intlShape.isRequired,
error: PropTypes.string.isRequired,
};
export default injectIntl(AccessBlocked);
export default AccessBlocked;

View File

@@ -1,41 +1,44 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Collapsible } from '@openedx/paragon';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import { getConfig } from '@edx/frontend-platform';
import messages from './IdVerification.messages';
const CameraHelp = (props) => (
<div>
<Collapsible
styling="card"
title={props.intl.formatMessage(messages['id.verification.camera.help.sight.question'])}
className="mb-4 shadow"
defaultOpen={props.isOpen}
>
<p>
{props.intl.formatMessage(messages[`id.verification.camera.help.sight.answer.${props.isPortrait ? 'portrait' : 'id'}`])}
</p>
</Collapsible>
<Collapsible
styling="card"
title={props.intl.formatMessage(messages[`id.verification.camera.help.difficulty.question.${props.isPortrait ? 'portrait' : 'id'}`])}
className="mb-4 shadow"
defaultOpen={props.isOpen}
>
<p>
{props.intl.formatMessage(
messages['id.verification.camera.help.difficulty.answer'],
{ siteName: getConfig().SITE_NAME },
)}
</p>
</Collapsible>
</div>
);
const CameraHelp = (props) => {
const intl = useIntl();
return (
<div>
<Collapsible
styling="card"
title={intl.formatMessage(messages['id.verification.camera.help.sight.question'])}
className="mb-4 shadow"
defaultOpen={props.isOpen}
>
<p>
{intl.formatMessage(messages[`id.verification.camera.help.sight.answer.${props.isPortrait ? 'portrait' : 'id'}`])}
</p>
</Collapsible>
<Collapsible
styling="card"
title={intl.formatMessage(messages[`id.verification.camera.help.difficulty.question.${props.isPortrait ? 'portrait' : 'id'}`])}
className="mb-4 shadow"
defaultOpen={props.isOpen}
>
<p>
{intl.formatMessage(
messages['id.verification.camera.help.difficulty.answer'],
{ siteName: getConfig().SITE_NAME },
)}
</p>
</Collapsible>
</div>
);
};
CameraHelp.propTypes = {
intl: intlShape.isRequired,
isOpen: PropTypes.bool,
isPortrait: PropTypes.bool,
};
@@ -45,4 +48,4 @@ CameraHelp.defaultProps = {
isPortrait: false,
};
export default injectIntl(CameraHelp);
export default CameraHelp;

View File

@@ -1,7 +1,7 @@
import React, { useState, useContext } from 'react';
import { useState, useContext } from 'react';
import PropTypes from 'prop-types';
import { Collapsible } from '@openedx/paragon';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import messages from './IdVerification.messages';
@@ -11,6 +11,7 @@ import ImagePreview from './ImagePreview';
import SupportedMediaTypes from './SupportedMediaTypes';
const CameraHelpWithUpload = (props) => {
const intl = useIntl();
const { setIdPhotoFile, idPhotoFile, userId } = useContext(IdVerificationContext);
const [hasUploadedImage, setHasUploadedImage] = useState(false);
@@ -27,24 +28,23 @@ const CameraHelpWithUpload = (props) => {
<div>
<Collapsible
styling="card"
title={props.intl.formatMessage(messages['id.verification.id.photo.unclear.question'])}
title={intl.formatMessage(messages['id.verification.id.photo.unclear.question'])}
data-testid="collapsible"
className="mb-4 shadow"
defaultOpen={props.isOpen}
>
{idPhotoFile && hasUploadedImage && <ImagePreview src={idPhotoFile} alt={props.intl.formatMessage(messages['id.verification.id.photo.preview.alt'])} />}
{idPhotoFile && hasUploadedImage && <ImagePreview src={idPhotoFile} alt={intl.formatMessage(messages['id.verification.id.photo.preview.alt'])} />}
<p>
{props.intl.formatMessage(messages['id.verification.id.photo.instructions.upload'])}
{intl.formatMessage(messages['id.verification.id.photo.instructions.upload'])}
<SupportedMediaTypes />
</p>
<ImageFileUpload onFileChange={setAndTrackIdPhotoFile} intl={props.intl} />
<ImageFileUpload onFileChange={setAndTrackIdPhotoFile} />
</Collapsible>
</div>
);
};
CameraHelpWithUpload.propTypes = {
intl: intlShape.isRequired,
isOpen: PropTypes.bool,
};
@@ -52,4 +52,4 @@ CameraHelpWithUpload.defaultProps = {
isOpen: false,
};
export default injectIntl(CameraHelpWithUpload);
export default CameraHelpWithUpload;

View File

@@ -1,12 +1,13 @@
import React, { useContext } from 'react';
import { useContext } from 'react';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Button, Collapsible } from '@openedx/paragon';
import IdVerificationContext from './IdVerificationContext';
import messages from './IdVerification.messages';
const CollapsibleImageHelp = (props) => {
const CollapsibleImageHelp = () => {
const intl = useIntl();
const {
userId, useCameraForId, setUseCameraForId,
} = useContext(IdVerificationContext);
@@ -25,15 +26,15 @@ const CollapsibleImageHelp = (props) => {
<Collapsible
styling="card"
title={useCameraForId
? props.intl.formatMessage(messages['id.verification.photo.upload.help.title'])
: props.intl.formatMessage(messages['id.verification.photo.camera.help.title'])}
? intl.formatMessage(messages['id.verification.photo.upload.help.title'])
: intl.formatMessage(messages['id.verification.photo.camera.help.title'])}
className="mb-4 shadow"
defaultOpen
>
<p data-testid="help-text">
{useCameraForId
? props.intl.formatMessage(messages['id.verification.photo.upload.help.text'])
: props.intl.formatMessage(messages['id.verification.photo.camera.help.text'])}
? intl.formatMessage(messages['id.verification.photo.upload.help.text'])
: intl.formatMessage(messages['id.verification.photo.camera.help.text'])}
</p>
<Button
title={useCameraForId ? 'Upload Photo' : 'Take Photo'} // TO-DO: translation
@@ -42,15 +43,11 @@ const CollapsibleImageHelp = (props) => {
style={{ marginTop: '0.5rem' }}
>
{useCameraForId
? props.intl.formatMessage(messages['id.verification.photo.upload.help.button'])
: props.intl.formatMessage(messages['id.verification.photo.camera.help.button'])}
? intl.formatMessage(messages['id.verification.photo.upload.help.button'])
: intl.formatMessage(messages['id.verification.photo.camera.help.button'])}
</Button>
</Collapsible>
);
};
CollapsibleImageHelp.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(CollapsibleImageHelp);
export default CollapsibleImageHelp;

View File

@@ -46,6 +46,11 @@ const messages = defineMessages({
defaultMessage: 'You need a valid identification card that contains your full name and photo, such as a drivers license or passport.',
description: 'Text that explains that the user needs a photo ID.',
},
'id.verification.privacy.modal.close.button': {
id: 'id.verification.privacy.modal.close.button',
defaultMessage: 'Close',
description: 'Label on button to close privacy information dialog.',
},
'id.verification.privacy.title': {
id: 'id.verification.privacy.title',
defaultMessage: 'Privacy Information',
@@ -656,6 +661,11 @@ const messages = defineMessages({
defaultMessage: 'Switch to Camera Mode',
description: 'Button used to switch to camera mode.',
},
'id.verification.context.loading.state': {
id: 'id.verification.context.loading.state',
defaultMessage: 'Loading verification status',
description: 'Message shown for screen readers when a user\'s identification verification is in the loading state',
},
});
export default messages;

View File

@@ -3,6 +3,7 @@ import React, {
} from 'react';
import PropTypes from 'prop-types';
import { AppContext } from '@edx/frontend-platform/react';
import { useIntl } from '@edx/frontend-platform/i18n';
import { getProfileDataManager } from '../account-settings/data/service';
import PageLoading from '../account-settings/PageLoading';
@@ -15,7 +16,10 @@ import { hasGetUserMediaSupport } from './getUserMediaShim';
import IdVerificationContext, { MEDIA_ACCESS, ERROR_REASONS, VERIFIED_MODES } from './IdVerificationContext';
import { VerifiedNameContext } from './VerifiedNameContext';
import messages from './IdVerification.messages';
const IdVerificationContextProvider = ({ children }) => {
const intl = useIntl();
const { authenticatedUser } = useContext(AppContext);
const { verifiedNameHistoryCallStatus, verifiedName } = useContext(VerifiedNameContext);
@@ -117,7 +121,7 @@ const IdVerificationContextProvider = ({ children }) => {
const loadingStatuses = [IDLE_STATUS, LOADING_STATUS];
// If we are waiting for verification status or verified name history endpoint, show spinner.
if (loadingStatuses.includes(idVerificationData.status) || loadingStatuses.includes(verifiedNameHistoryCallStatus)) {
return <PageLoading srMessage="Loading verification status" />;
return <PageLoading srMessage={intl.formatMessage(messages['id.verification.context.loading.state'])} />;
}
if (!canVerify) {

View File

@@ -1,11 +1,11 @@
import React, { useState, useEffect } from 'react';
import { useState, useEffect } from 'react';
import { connect } from 'react-redux';
import {
Route, Routes, useLocation, useNavigate,
} from 'react-router-dom';
import camelCase from 'lodash.camelcase';
import qs from 'qs';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Button, ModalDialog, ActionRow } from '@openedx/paragon';
import { getConfig } from '@edx/frontend-platform';
import { idVerificationSelector } from './data/selectors';
@@ -26,9 +26,10 @@ import SubmittedPanel from './panels/SubmittedPanel';
import messages from './IdVerification.messages';
// eslint-disable-next-line react/prefer-stateless-function
const IdVerificationPage = (props) => {
const IdVerificationPage = () => {
const { search } = useLocation();
const navigate = useNavigate();
const intl = useIntl();
const [isModalOpen, setIsModalOpen] = useState(false);
@@ -78,33 +79,33 @@ const IdVerificationPage = (props) => {
</div>
<ModalDialog
isOpen={isModalOpen}
title="Id modal"
title={intl.formatMessage(messages['id.verification.privacy.title'])}
onClose={() => setIsModalOpen(false)}
size="lg"
hasCloseButton={false}
>
<ModalDialog.Header>
<ModalDialog.Title data-testid="Id-modal">
{props.intl.formatMessage(messages['id.verification.privacy.title'])}
{intl.formatMessage(messages['id.verification.privacy.title'])}
</ModalDialog.Title>
</ModalDialog.Header>
<ModalDialog.Body>
<div className="p-3">
<h6>
{props.intl.formatMessage(
{intl.formatMessage(
messages['id.verification.privacy.need.photo.question'],
{ siteName: getConfig().SITE_NAME },
)}
</h6>
<p>{props.intl.formatMessage(messages['id.verification.privacy.need.photo.answer'])}</p>
<p>{intl.formatMessage(messages['id.verification.privacy.need.photo.answer'])}</p>
<h6>
{props.intl.formatMessage(
{intl.formatMessage(
messages['id.verification.privacy.do.with.photo.question'],
{ siteName: getConfig().SITE_NAME },
)}
</h6>
<p>
{props.intl.formatMessage(
{intl.formatMessage(
messages['id.verification.privacy.do.with.photo.answer'],
{ siteName: getConfig().SITE_NAME },
)}
@@ -114,7 +115,7 @@ const IdVerificationPage = (props) => {
<ModalDialog.Footer className="p-2">
<ActionRow>
<ModalDialog.CloseButton variant="link">
Close
{intl.formatMessage(messages['id.verification.privacy.modal.close.button'])}
</ModalDialog.CloseButton>
</ActionRow>
</ModalDialog.Footer>
@@ -124,8 +125,4 @@ const IdVerificationPage = (props) => {
);
};
IdVerificationPage.propTypes = {
intl: intlShape.isRequired,
};
export default connect(idVerificationSelector, {})(injectIntl(IdVerificationPage));
export default connect(idVerificationSelector, {})(IdVerificationPage);

View File

@@ -1,11 +1,12 @@
import React, { useCallback, useState } from 'react';
import { intlShape } from '@edx/frontend-platform/i18n';
import { useCallback, useState } from 'react';
import { useIntl } from '@edx/frontend-platform/i18n';
import PropTypes from 'prop-types';
import { Alert } from '@openedx/paragon';
import messages from './IdVerification.messages';
import SupportedMediaTypes from './SupportedMediaTypes';
const ImageFileUpload = ({ onFileChange, intl }) => {
const ImageFileUpload = ({ onFileChange }) => {
const intl = useIntl();
const [error, setError] = useState(null);
const errorTypes = {
invalidFileType: 'invalidFileType',
@@ -58,7 +59,6 @@ const ImageFileUpload = ({ onFileChange, intl }) => {
ImageFileUpload.propTypes = {
onFileChange: PropTypes.func.isRequired,
intl: intlShape.isRequired,
};
export default ImageFileUpload;

View File

@@ -0,0 +1,147 @@
import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import qs from 'qs';
import { getExistingIdVerification, getEnrollments, submitIdVerification } from './service';
jest.mock('@edx/frontend-platform', () => {
const actual = jest.requireActual('@edx/frontend-platform');
return {
...actual,
getConfig: jest.fn(),
};
});
jest.mock('@edx/frontend-platform/auth', () => ({
getAuthenticatedHttpClient: jest.fn(),
}));
jest.mock('qs');
describe('ID Verification Service', () => {
let mockHttpClient;
beforeEach(() => {
jest.resetAllMocks();
getConfig.mockReturnValue({ LMS_BASE_URL: 'http://test.lms' });
mockHttpClient = {
get: jest.fn(),
post: jest.fn(),
};
getAuthenticatedHttpClient.mockReturnValue(mockHttpClient);
});
describe('getExistingIdVerification', () => {
it('returns transformed data on success', async () => {
const mockResponse = {
data: { status: 'approved', expires: '2025-12-01', can_verify: true },
};
mockHttpClient.get.mockResolvedValue(mockResponse);
const result = await getExistingIdVerification();
expect(mockHttpClient.get).toHaveBeenCalledWith(
'http://test.lms/verify_student/status/',
{ headers: { Accept: 'application/json' } },
);
expect(result).toEqual({
status: 'approved',
expires: '2025-12-01',
canVerify: true,
});
});
it('returns defaults on failure', async () => {
mockHttpClient.get.mockRejectedValue(new Error('Network error'));
const result = await getExistingIdVerification();
expect(result).toEqual({
status: null,
expires: null,
canVerify: false,
});
});
});
describe('getEnrollments', () => {
it('returns data on success', async () => {
const mockResponse = { data: [{ course_id: 'course-v1:test+T101+2025', mode: 'verified' }] };
mockHttpClient.get.mockResolvedValue(mockResponse);
const result = await getEnrollments();
expect(mockHttpClient.get).toHaveBeenCalledWith(
'http://test.lms/api/enrollment/v1/enrollment',
{ headers: { Accept: 'application/json' } },
);
expect(result).toEqual(mockResponse.data);
});
it('returns empty object on failure', async () => {
mockHttpClient.get.mockRejectedValue(new Error('Server error'));
const result = await getEnrollments();
expect(result).toEqual({});
});
});
describe('submitIdVerification', () => {
it('posts transformed data and returns success', async () => {
const verificationData = {
facePhotoFile: 'face-img',
idPhotoFile: 'id-img',
idPhotoName: 'John Doe',
courseRunKey: 'course-v1:test+T101+2025',
};
const expectedPostData = {
face_image: 'face-img',
photo_id_image: 'id-img',
full_name: 'John Doe',
};
qs.stringify.mockReturnValue('encoded-data');
mockHttpClient.post.mockResolvedValue({});
const result = await submitIdVerification(verificationData);
expect(qs.stringify).toHaveBeenCalledWith(expectedPostData);
expect(mockHttpClient.post).toHaveBeenCalledWith(
'http://test.lms/verify_student/submit-photos/',
'encoded-data',
{ headers: { 'Content-Type': 'application/x-www-form-urlencoded' } },
);
expect(result).toEqual({ success: true, message: null });
});
it('omits null/undefined values', async () => {
const verificationData = {
facePhotoFile: 'face-img',
idPhotoFile: null,
idPhotoName: undefined,
};
qs.stringify.mockReturnValue('encoded-data');
mockHttpClient.post.mockResolvedValue({});
await submitIdVerification(verificationData);
expect(qs.stringify).toHaveBeenCalledWith({ face_image: 'face-img' });
});
it('returns failure object on error', async () => {
const error = new Error('Failed');
error.customAttributes = { httpErrorStatus: 400 };
mockHttpClient.post.mockRejectedValue(error);
const result = await submitIdVerification({ facePhotoFile: 'face-img' });
expect(result).toEqual({
success: false,
status: 400,
message: expect.stringContaining('Failed'),
});
});
});
});

View File

@@ -1,17 +1,17 @@
import PropTypes from 'prop-types';
import React from 'react';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import messages from '../IdVerification.messages';
export const EnableCameraDirectionsPanel = (props) => {
const intl = useIntl();
if (props.browserName === 'Internet Explorer') {
return (
<>
<h6>{props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.ie11'])}</h6>
<h6>{intl.formatMessage(messages['id.verification.camera.access.failure.temporary.ie11'])}</h6>
<ol>
<li>{props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.ie11.step1'])}</li>
<li>{props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.ie11.step2'])}</li>
<li>{props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.ie11.step3'])}</li>
<li>{intl.formatMessage(messages['id.verification.camera.access.failure.temporary.ie11.step1'])}</li>
<li>{intl.formatMessage(messages['id.verification.camera.access.failure.temporary.ie11.step2'])}</li>
<li>{intl.formatMessage(messages['id.verification.camera.access.failure.temporary.ie11.step3'])}</li>
</ol>
</>
);
@@ -19,17 +19,17 @@ export const EnableCameraDirectionsPanel = (props) => {
if (props.browserName === 'Chrome') {
return (
<>
<h6>{props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.chrome'])}</h6>
<h6>{intl.formatMessage(messages['id.verification.camera.access.failure.temporary.chrome'])}</h6>
<ol>
<li>{props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.chrome.step1'])}</li>
<li>{props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.chrome.step2'])}</li>
<li>{intl.formatMessage(messages['id.verification.camera.access.failure.temporary.chrome.step1'])}</li>
<li>{intl.formatMessage(messages['id.verification.camera.access.failure.temporary.chrome.step2'])}</li>
<ul>
<li>{props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.chrome.step2.windows'])}</li>
<li>{props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.chrome.step2.mac'])}</li>
<li>{intl.formatMessage(messages['id.verification.camera.access.failure.temporary.chrome.step2.windows'])}</li>
<li>{intl.formatMessage(messages['id.verification.camera.access.failure.temporary.chrome.step2.mac'])}</li>
</ul>
<li>{props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.chrome.step3'])}</li>
<li>{props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.chrome.step4'])}</li>
<li>{props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.chrome.step5'])}</li>
<li>{intl.formatMessage(messages['id.verification.camera.access.failure.temporary.chrome.step3'])}</li>
<li>{intl.formatMessage(messages['id.verification.camera.access.failure.temporary.chrome.step4'])}</li>
<li>{intl.formatMessage(messages['id.verification.camera.access.failure.temporary.chrome.step5'])}</li>
</ol>
</>
);
@@ -37,15 +37,15 @@ export const EnableCameraDirectionsPanel = (props) => {
if (props.browserName === 'Firefox') {
return (
<>
<h6>{props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.firefox'])}</h6>
<h6>{intl.formatMessage(messages['id.verification.camera.access.failure.temporary.firefox'])}</h6>
<ol>
<li>{props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.firefox.step1'])}</li>
<li>{props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.firefox.step2'])}</li>
<li>{props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.firefox.step3'])}</li>
<li>{props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.firefox.step4'])}</li>
<li>{props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.firefox.step5'])}</li>
<li>{props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.firefox.step6'])}</li>
<li>{props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.firefox.step7'])}</li>
<li>{intl.formatMessage(messages['id.verification.camera.access.failure.temporary.firefox.step1'])}</li>
<li>{intl.formatMessage(messages['id.verification.camera.access.failure.temporary.firefox.step2'])}</li>
<li>{intl.formatMessage(messages['id.verification.camera.access.failure.temporary.firefox.step3'])}</li>
<li>{intl.formatMessage(messages['id.verification.camera.access.failure.temporary.firefox.step4'])}</li>
<li>{intl.formatMessage(messages['id.verification.camera.access.failure.temporary.firefox.step5'])}</li>
<li>{intl.formatMessage(messages['id.verification.camera.access.failure.temporary.firefox.step6'])}</li>
<li>{intl.formatMessage(messages['id.verification.camera.access.failure.temporary.firefox.step7'])}</li>
</ol>
</>
);
@@ -53,12 +53,12 @@ export const EnableCameraDirectionsPanel = (props) => {
if (props.browserName === 'Safari') {
return (
<>
<h6>{props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.safari'])}</h6>
<h6>{intl.formatMessage(messages['id.verification.camera.access.failure.temporary.safari'])}</h6>
<ol>
<li>{props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.safari.step1'])}</li>
<li>{props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.safari.step2'])}</li>
<li>{props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.safari.step3'])}</li>
<li>{props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.safari.step4'])}</li>
<li>{intl.formatMessage(messages['id.verification.camera.access.failure.temporary.safari.step1'])}</li>
<li>{intl.formatMessage(messages['id.verification.camera.access.failure.temporary.safari.step2'])}</li>
<li>{intl.formatMessage(messages['id.verification.camera.access.failure.temporary.safari.step3'])}</li>
<li>{intl.formatMessage(messages['id.verification.camera.access.failure.temporary.safari.step4'])}</li>
</ol>
</>
);
@@ -68,8 +68,7 @@ export const EnableCameraDirectionsPanel = (props) => {
};
EnableCameraDirectionsPanel.propTypes = {
intl: intlShape.isRequired,
browserName: PropTypes.string.isRequired,
};
export default injectIntl(EnableCameraDirectionsPanel);
export default EnableCameraDirectionsPanel;

View File

@@ -1,9 +1,9 @@
import React, {
import {
useContext, useEffect, useRef,
} from 'react';
import { Form } from '@openedx/paragon';
import { Link, useNavigate, useLocation } from 'react-router-dom';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import { useNextPanelSlug } from '../routing-utilities';
import BasePanel from './BasePanel';
@@ -11,12 +11,13 @@ import IdVerificationContext from '../IdVerificationContext';
import messages from '../IdVerification.messages';
const GetNameIdPanel = (props) => {
const GetNameIdPanel = () => {
const location = useLocation();
const navigate = useNavigate();
const nameInputRef = useRef();
const panelSlug = 'get-name-id';
const nextPanelSlug = useNextPanelSlug(panelSlug);
const intl = useIntl();
const { nameOnAccount, idPhotoName, setIdPhotoName } = useContext(IdVerificationContext);
const nameOnAccountValue = nameOnAccount || '';
@@ -41,19 +42,19 @@ const GetNameIdPanel = (props) => {
return (
<BasePanel
name={panelSlug}
title={props.intl.formatMessage(messages['id.verification.name.check.title'])}
title={intl.formatMessage(messages['id.verification.name.check.title'])}
>
<p>
{props.intl.formatMessage(messages['id.verification.name.check.instructions'])}
{intl.formatMessage(messages['id.verification.name.check.instructions'])}
</p>
<p>
{props.intl.formatMessage(messages['id.verification.name.check.mismatch.information'])}
{intl.formatMessage(messages['id.verification.name.check.mismatch.information'])}
</p>
<Form onSubmit={handleSubmit}>
<Form.Group>
<Form.Label className="font-weight-bold" htmlFor="photo-id-name">
{props.intl.formatMessage(messages['id.verification.name.label'])}
{intl.formatMessage(messages['id.verification.name.label'])}
</Form.Label>
<Form.Control
controlId="photo-id-name"
@@ -72,7 +73,7 @@ const GetNameIdPanel = (props) => {
data-testid="id-name-feedback-message"
type="invalid"
>
{props.intl.formatMessage(messages['id.verification.name.error'])}
{intl.formatMessage(messages['id.verification.name.error'])}
</Form.Control.Feedback>
)}
</Form.Group>
@@ -85,15 +86,11 @@ const GetNameIdPanel = (props) => {
data-testid="next-button"
aria-disabled={!idPhotoName}
>
{props.intl.formatMessage(messages['id.verification.next'])}
{intl.formatMessage(messages['id.verification.next'])}
</Link>
</div>
</BasePanel>
);
};
GetNameIdPanel.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(GetNameIdPanel);
export default GetNameIdPanel;

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import { useNextPanelSlug } from '../routing-utilities';
import BasePanel from './BasePanel';
@@ -8,49 +8,46 @@ import CameraHelp from '../CameraHelp';
import messages from '../IdVerification.messages';
import exampleCard from '../assets/example-card.png';
const IdContextPanel = (props) => {
const IdContextPanel = () => {
const panelSlug = 'id-context';
const nextPanelSlug = useNextPanelSlug(panelSlug);
const intl = useIntl();
return (
<BasePanel
name={panelSlug}
title={props.intl.formatMessage(messages['id.verification.id.tips.title'])}
title={intl.formatMessage(messages['id.verification.id.tips.title'])}
>
<p>{props.intl.formatMessage(messages['id.verification.id.tips.description'])}</p>
<p>{intl.formatMessage(messages['id.verification.id.tips.description'])}</p>
<div className="card mb-4 shadow accent border-warning">
<div className="card-body">
<h6>
{props.intl.formatMessage(messages['id.verification.photo.tips.list.title'])}
{intl.formatMessage(messages['id.verification.photo.tips.list.title'])}
</h6>
<p>
{props.intl.formatMessage(messages['id.verification.photo.tips.list.description'])}
{intl.formatMessage(messages['id.verification.photo.tips.list.description'])}
</p>
<ul>
<li>
{props.intl.formatMessage(messages['id.verification.id.tips.list.well.lit'])}
{intl.formatMessage(messages['id.verification.id.tips.list.well.lit'])}
</li>
<li>
{props.intl.formatMessage(messages['id.verification.id.tips.list.clear'])}
{intl.formatMessage(messages['id.verification.id.tips.list.clear'])}
</li>
</ul>
<img
src={exampleCard}
alt={props.intl.formatMessage(messages['id.verification.example.card.alt'])}
alt={intl.formatMessage(messages['id.verification.example.card.alt'])}
/>
</div>
</div>
<CameraHelp isOpen />
<div className="action-row">
<Link to={`/id-verification/${nextPanelSlug}`} className="btn btn-primary" data-testid="next-button">
{props.intl.formatMessage(messages['id.verification.next'])}
{intl.formatMessage(messages['id.verification.next'])}
</Link>
</div>
</BasePanel>
);
};
IdContextPanel.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(IdContextPanel);
export default IdContextPanel;

View File

@@ -1,37 +1,37 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import { useNextPanelSlug } from '../routing-utilities';
import BasePanel from './BasePanel';
import CameraHelp from '../CameraHelp';
import messages from '../IdVerification.messages';
const PortraitPhotoContextPanel = (props) => {
const PortraitPhotoContextPanel = () => {
const intl = useIntl();
const panelSlug = 'portrait-photo-context';
const nextPanelSlug = useNextPanelSlug(panelSlug);
return (
<BasePanel
name={panelSlug}
title={props.intl.formatMessage(messages['id.verification.photo.tips.title'])}
title={intl.formatMessage(messages['id.verification.photo.tips.title'])}
>
<p>
{props.intl.formatMessage(messages['id.verification.photo.tips.description'])}
{intl.formatMessage(messages['id.verification.photo.tips.description'])}
</p>
<div className="card mb-4 shadow accent border-warning">
<div className="card-body">
<h6>
{props.intl.formatMessage(messages['id.verification.photo.tips.list.title'])}
{intl.formatMessage(messages['id.verification.photo.tips.list.title'])}
</h6>
<p>
{props.intl.formatMessage(messages['id.verification.photo.tips.list.description'])}
{intl.formatMessage(messages['id.verification.photo.tips.list.description'])}
</p>
<ul className="mb-0">
<li>
{props.intl.formatMessage(messages['id.verification.photo.tips.list.well.lit'])}
{intl.formatMessage(messages['id.verification.photo.tips.list.well.lit'])}
</li>
<li>
{props.intl.formatMessage(messages['id.verification.photo.tips.list.inside.frame'])}
{intl.formatMessage(messages['id.verification.photo.tips.list.inside.frame'])}
</li>
</ul>
</div>
@@ -39,15 +39,11 @@ const PortraitPhotoContextPanel = (props) => {
<CameraHelp isOpen isPortrait />
<div className="action-row">
<Link to={`/id-verification/${nextPanelSlug}`} className="btn btn-primary" data-testid="next-button">
{props.intl.formatMessage(messages['id.verification.next'])}
{intl.formatMessage(messages['id.verification.next'])}
</Link>
</div>
</BasePanel>
);
};
PortraitPhotoContextPanel.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(PortraitPhotoContextPanel);
export default PortraitPhotoContextPanel;

View File

@@ -1,9 +1,9 @@
import React, { useEffect, useContext } from 'react';
import { useEffect, useContext } from 'react';
import { Link } from 'react-router-dom';
import Bowser from 'bowser';
import { getConfig } from '@edx/frontend-platform';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
import { useRedirect } from '../../hooks';
import { useNextPanelSlug } from '../routing-utilities';
@@ -14,7 +14,8 @@ import { UnsupportedCameraDirectionsPanel } from './UnsupportedCameraDirectionsP
import messages from '../IdVerification.messages';
const RequestCameraAccessPanel = (props) => {
const RequestCameraAccessPanel = () => {
const intl = useIntl();
const { location: returnUrl, text: returnText } = useRedirect();
const panelSlug = 'request-camera-access';
const nextPanelSlug = useNextPanelSlug(panelSlug);
@@ -40,17 +41,17 @@ const RequestCameraAccessPanel = (props) => {
const getTitle = () => {
if (mediaAccess === MEDIA_ACCESS.GRANTED) {
return props.intl.formatMessage(messages['id.verification.camera.access.title.success']);
return intl.formatMessage(messages['id.verification.camera.access.title.success']);
}
if ([MEDIA_ACCESS.UNSUPPORTED, MEDIA_ACCESS.DENIED].includes(mediaAccess)) {
return props.intl.formatMessage(messages['id.verification.camera.access.title.failed']);
return intl.formatMessage(messages['id.verification.camera.access.title.failed']);
}
return props.intl.formatMessage(messages['id.verification.camera.access.title']);
return intl.formatMessage(messages['id.verification.camera.access.title']);
};
const returnLink = (
<a className="btn btn-primary" href={`${getConfig().LMS_BASE_URL}/${returnUrl}`}>
{props.intl.formatMessage(messages[returnText])}
{intl.formatMessage(messages[returnText])}
</a>
);
@@ -67,13 +68,13 @@ const RequestCameraAccessPanel = (props) => {
defaultMessage="In order to take a photo using your webcam, you may receive a browser prompt for access to your camera. {clickAllow}"
description="Instructions to enable camera access."
values={{
clickAllow: <strong>{props.intl.formatMessage(messages['id.verification.camera.access.click.allow'])}</strong>,
clickAllow: <strong>{intl.formatMessage(messages['id.verification.camera.access.click.allow'])}</strong>,
}}
/>
</p>
<div className="action-row">
<button type="button" className="btn btn-primary" onClick={tryGetUserMedia}>
{props.intl.formatMessage(messages['id.verification.camera.access.enable'])}
{intl.formatMessage(messages['id.verification.camera.access.enable'])}
</button>
</div>
</div>
@@ -82,11 +83,11 @@ const RequestCameraAccessPanel = (props) => {
{mediaAccess === MEDIA_ACCESS.GRANTED && (
<div>
<p data-testid="camera-access-success">
{props.intl.formatMessage(messages['id.verification.camera.access.success'])}
{intl.formatMessage(messages['id.verification.camera.access.success'])}
</p>
<div className="action-row">
<Link to={`/id-verification/${nextPanelSlug}`} className="btn btn-primary" data-testid="next-button">
{props.intl.formatMessage(messages['id.verification.next'])}
{intl.formatMessage(messages['id.verification.next'])}
</Link>
</div>
</div>
@@ -95,9 +96,9 @@ const RequestCameraAccessPanel = (props) => {
{mediaAccess === MEDIA_ACCESS.DENIED && (
<div data-testid="camera-failure-instructions">
<p data-testid="camera-access-failure">
{props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary'])}
{intl.formatMessage(messages['id.verification.camera.access.failure.temporary'])}
</p>
<EnableCameraDirectionsPanel browserName={browserName} intl={props.intl} />
<EnableCameraDirectionsPanel browserName={browserName} />
<div className="action-row">
{returnLink}
</div>
@@ -107,9 +108,9 @@ const RequestCameraAccessPanel = (props) => {
{mediaAccess === MEDIA_ACCESS.UNSUPPORTED && (
<div data-testid="camera-unsupported-instructions">
<p data-testid="camera-unsupported-failure">
{props.intl.formatMessage(messages['id.verification.camera.access.failure.unsupported'])}
{intl.formatMessage(messages['id.verification.camera.access.failure.unsupported'])}
</p>
<UnsupportedCameraDirectionsPanel browserName={browserName} intl={props.intl} />
<UnsupportedCameraDirectionsPanel browserName={browserName} />
<div className="action-row">
{returnLink}
</div>
@@ -120,8 +121,4 @@ const RequestCameraAccessPanel = (props) => {
);
};
RequestCameraAccessPanel.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(RequestCameraAccessPanel);
export default RequestCameraAccessPanel;

View File

@@ -1,8 +1,8 @@
import React, { useEffect, useContext } from 'react';
import { useEffect, useContext } from 'react';
import { Link } from 'react-router-dom';
import { getConfig } from '@edx/frontend-platform';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
import { Alert, Hyperlink } from '@openedx/paragon';
import { useNextPanelSlug } from '../routing-utilities';
@@ -12,7 +12,8 @@ import IdVerificationContext from '../IdVerificationContext';
import messages from '../IdVerification.messages';
import exampleCard from '../assets/example-card.png';
const ReviewRequirementsPanel = (props) => {
const ReviewRequirementsPanel = () => {
const intl = useIntl();
const { userId, profileDataManager } = useContext(IdVerificationContext);
const panelSlug = 'review-requirements';
const nextPanelSlug = useNextPanelSlug(panelSlug);
@@ -41,7 +42,7 @@ const ReviewRequirementsPanel = (props) => {
profileDataManager,
support: (
<Hyperlink destination={getConfig().SUPPORT_URL} target="_blank">
{props.intl.formatMessage(messages['id.verification.support'])}
{intl.formatMessage(messages['id.verification.support'])}
</Hyperlink>
),
}}
@@ -54,17 +55,17 @@ const ReviewRequirementsPanel = (props) => {
return (
<BasePanel
name={panelSlug}
title={props.intl.formatMessage(messages['id.verification.requirements.title'])}
title={intl.formatMessage(messages['id.verification.requirements.title'])}
focusOnMount={false}
>
{renderManagedProfileMessage()}
<p>
{props.intl.formatMessage(messages['id.verification.requirements.description'])}
{intl.formatMessage(messages['id.verification.requirements.description'])}
</p>
<div className="card mb-4 shadow accent border-warning">
<div className="card-body">
<h6 aria-level="3">
{props.intl.formatMessage(messages['id.verification.requirements.card.device.title'])}
{intl.formatMessage(messages['id.verification.requirements.card.device.title'])}
</h6>
<p className="mb-0">
<FormattedMessage
@@ -72,7 +73,7 @@ const ReviewRequirementsPanel = (props) => {
defaultMessage="You need a device that has a camera. If you receive a browser prompt for access to your camera, please make sure to click {allow}."
description="Text explaining that the user needs access to a camera."
values={{
allow: <strong>{props.intl.formatMessage(messages['id.verification.requirements.card.device.allow'])}</strong>,
allow: <strong>{intl.formatMessage(messages['id.verification.requirements.card.device.allow'])}</strong>,
}}
/>
</p>
@@ -81,37 +82,37 @@ const ReviewRequirementsPanel = (props) => {
<div className="card mb-4 shadow accent border-warning">
<div className="card-body">
<h6 aria-level="3">
{props.intl.formatMessage(messages['id.verification.requirements.card.id.title'])}
{intl.formatMessage(messages['id.verification.requirements.card.id.title'])}
</h6>
<p className="mb-0">
{props.intl.formatMessage(messages['id.verification.requirements.card.id.text'])}
{intl.formatMessage(messages['id.verification.requirements.card.id.text'])}
<img
src={exampleCard}
alt={props.intl.formatMessage(messages['id.verification.example.card.alt'])}
alt={intl.formatMessage(messages['id.verification.example.card.alt'])}
/>
</p>
</div>
</div>
<h4 aria-level="2" className="mb-3">
{props.intl.formatMessage(messages['id.verification.privacy.title'])}
{intl.formatMessage(messages['id.verification.privacy.title'])}
</h4>
<h6 aria-level="3">
{props.intl.formatMessage(
{intl.formatMessage(
messages['id.verification.privacy.need.photo.question'],
{ siteName: getConfig().SITE_NAME },
)}
</h6>
<p>
{props.intl.formatMessage(messages['id.verification.privacy.need.photo.answer'])}
{intl.formatMessage(messages['id.verification.privacy.need.photo.answer'])}
</p>
<h6 aria-level="3">
{props.intl.formatMessage(
{intl.formatMessage(
messages['id.verification.privacy.do.with.photo.question'],
{ siteName: getConfig().SITE_NAME },
)}
</h6>
<p>
{props.intl.formatMessage(
{intl.formatMessage(
messages['id.verification.privacy.do.with.photo.answer'],
{ siteName: getConfig().SITE_NAME },
)}
@@ -119,15 +120,11 @@ const ReviewRequirementsPanel = (props) => {
<div className="action-row">
<Link to={`/id-verification/${nextPanelSlug}`} className="btn btn-primary" data-testid="next-button">
{props.intl.formatMessage(messages['id.verification.next'])}
{intl.formatMessage(messages['id.verification.next'])}
</Link>
</div>
</BasePanel>
);
};
ReviewRequirementsPanel.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(ReviewRequirementsPanel);
export default ReviewRequirementsPanel;

View File

@@ -1,7 +1,7 @@
import React, { useContext, useEffect } from 'react';
import { useContext, useEffect } from 'react';
import { getConfig } from '@edx/frontend-platform';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import { useRedirect } from '../../hooks';
@@ -10,10 +10,11 @@ import messages from '../IdVerification.messages';
import BasePanel from './BasePanel';
const SubmittedPanel = (props) => {
const SubmittedPanel = () => {
const { userId } = useContext(IdVerificationContext);
const { location: returnUrl, text: returnText } = useRedirect();
const panelSlug = 'submitted';
const intl = useIntl();
useEffect(() => {
sendTrackEvent('edx.id_verification.submitted', {
@@ -25,24 +26,20 @@ const SubmittedPanel = (props) => {
return (
<BasePanel
name={panelSlug}
title={props.intl.formatMessage(messages['id.verification.submitted.title'])}
title={intl.formatMessage(messages['id.verification.submitted.title'])}
>
<p>
{props.intl.formatMessage(messages['id.verification.submitted.text'])}
{intl.formatMessage(messages['id.verification.submitted.text'])}
</p>
<a
className="btn btn-primary"
href={`${getConfig().LMS_BASE_URL}/${returnUrl}`}
data-testid="return-button"
>
{props.intl.formatMessage(messages[returnText])}
{intl.formatMessage(messages[returnText])}
</a>
</BasePanel>
);
};
SubmittedPanel.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(SubmittedPanel);
export default SubmittedPanel;

View File

@@ -1,10 +1,10 @@
import React, { useState, useContext, useEffect } from 'react';
import { useState, useContext, useEffect } from 'react';
import { getConfig } from '@edx/frontend-platform';
import {
Alert, Hyperlink, Form, Button, Spinner,
} from '@openedx/paragon';
import { Link, useNavigate } from 'react-router-dom';
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
import { submitIdVerification } from '../data/service';
import { useNextPanelSlug } from '../routing-utilities';
@@ -16,7 +16,8 @@ import messages from '../IdVerification.messages';
import CameraHelpWithUpload from '../CameraHelpWithUpload';
import SupportedMediaTypes from '../SupportedMediaTypes';
const SummaryPanel = (props) => {
const SummaryPanel = () => {
const intl = useIntl();
const panelSlug = 'summary';
const nextPanelSlug = useNextPanelSlug(panelSlug);
const {
@@ -51,7 +52,7 @@ const SummaryPanel = (props) => {
profileDataManager,
support: (
<Hyperlink destination={getConfig().SUPPORT_URL} target="_blank">
{props.intl.formatMessage(messages['id.verification.support'])}
{intl.formatMessage(messages['id.verification.support'])}
</Hyperlink>
),
}}
@@ -90,12 +91,11 @@ const SummaryPanel = (props) => {
};
return (
<Button
title="Confirmation"
disabled={isSubmitting}
onClick={handleClick}
data-testid="submit-button"
>
{props.intl.formatMessage(messages['id.verification.review.confirm'])}
{intl.formatMessage(messages['id.verification.review.confirm'])}
</Button>
);
};
@@ -103,18 +103,18 @@ const SummaryPanel = (props) => {
function getError() {
if (submissionError.status === 400) {
if (submissionError.message.includes('face_image')) {
return props.intl.formatMessage(messages['id.verification.submission.alert.error.face']);
return intl.formatMessage(messages['id.verification.submission.alert.error.face']);
}
if (submissionError.message.includes('Photo ID image')) {
return props.intl.formatMessage(messages['id.verification.submission.alert.error.id']);
return intl.formatMessage(messages['id.verification.submission.alert.error.id']);
}
if (submissionError.message.includes('Name')) {
return props.intl.formatMessage(messages['id.verification.submission.alert.error.name']);
return intl.formatMessage(messages['id.verification.submission.alert.error.name']);
}
if (submissionError.message.includes('unsupported format')) {
return (
<>
{props.intl.formatMessage(messages['id.verification.submission.alert.error.unsupported'])}
{intl.formatMessage(messages['id.verification.submission.alert.error.unsupported'])}
<SupportedMediaTypes />
</>
);
@@ -131,7 +131,7 @@ const SummaryPanel = (props) => {
values={{
support_link: (
<Alert.Link href="https://support.edx.org/hc/en-us">
{props.intl.formatMessage(
{intl.formatMessage(
messages['id.verification.review.error'],
{ siteName: getConfig().SITE_NAME },
)}
@@ -145,7 +145,7 @@ const SummaryPanel = (props) => {
return (
<BasePanel
name={panelSlug}
title={props.intl.formatMessage(messages['id.verification.review.title'])}
title={intl.formatMessage(messages['id.verification.review.title'])}
>
{submissionError && (
<Alert
@@ -158,17 +158,17 @@ const SummaryPanel = (props) => {
</Alert>
)}
<p>
{props.intl.formatMessage(messages['id.verification.review.description'])}
{intl.formatMessage(messages['id.verification.review.description'])}
</p>
<div className="row mb-4">
<div className="col-6">
<label htmlFor="photo-of-face" className="font-weight-bold">
{props.intl.formatMessage(messages['id.verification.review.portrait.label'])}
{intl.formatMessage(messages['id.verification.review.portrait.label'])}
</label>
<ImagePreview
id="photo-of-face"
src={facePhotoFile}
alt={props.intl.formatMessage(messages['id.verification.review.portrait.alt'])}
alt={intl.formatMessage(messages['id.verification.review.portrait.alt'])}
/>
<Link
className="btn btn-outline-primary"
@@ -176,17 +176,17 @@ const SummaryPanel = (props) => {
state={{ fromSummary: true }}
data-testid="portrait-retake"
>
{props.intl.formatMessage(messages['id.verification.review.portrait.retake'])}
{intl.formatMessage(messages['id.verification.review.portrait.retake'])}
</Link>
</div>
<div className="col-6">
<label htmlFor="photo-of-id/edit" className="font-weight-bold">
{props.intl.formatMessage(messages['id.verification.review.id.label'])}
{intl.formatMessage(messages['id.verification.review.id.label'])}
</label>
<ImagePreview
id="photo-of-id"
src={idPhotoFile}
alt={props.intl.formatMessage(messages['id.verification.review.id.alt'])}
alt={intl.formatMessage(messages['id.verification.review.id.alt'])}
/>
<Link
className="btn btn-outline-primary"
@@ -194,14 +194,14 @@ const SummaryPanel = (props) => {
state={{ fromSummary: true }}
data-testid="id-retake"
>
{props.intl.formatMessage(messages['id.verification.review.id.retake'])}
{intl.formatMessage(messages['id.verification.review.id.retake'])}
</Link>
</div>
</div>
<CameraHelpWithUpload />
<div className="form-group">
<label htmlFor="name-to-be-used" className="font-weight-bold">
{props.intl.formatMessage(messages['id.verification.name.label'])}
{intl.formatMessage(messages['id.verification.name.label'])}
</label>
{renderManagedProfileMessage()}
<div className="d-flex">
@@ -237,8 +237,4 @@ const SummaryPanel = (props) => {
);
};
SummaryPanel.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(SummaryPanel);
export default SummaryPanel;

View File

@@ -1,6 +1,6 @@
import React, { useContext, useEffect, useState } from 'react';
import { useContext, useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import { useNextPanelSlug } from '../routing-utilities';
import BasePanel from './BasePanel';
@@ -14,11 +14,12 @@ import ImageFileUpload from '../ImageFileUpload';
import CollapsibleImageHelp from '../CollapsibleImageHelp';
import SupportedMediaTypes from '../SupportedMediaTypes';
const TakeIdPhotoPanel = (props) => {
const TakeIdPhotoPanel = () => {
const panelSlug = 'take-id-photo';
const nextPanelSlug = useNextPanelSlug(panelSlug);
const { setIdPhotoFile, idPhotoFile, useCameraForId } = useContext(IdVerificationContext);
const [mounted, setMounted] = useState(false);
const intl = useIntl();
useEffect(() => {
// This prevents focus switching to the heading when taking a photo
@@ -30,31 +31,31 @@ const TakeIdPhotoPanel = (props) => {
name={panelSlug}
focusOnMount={!mounted}
title={useCameraForId
? props.intl.formatMessage(messages['id.verification.id.photo.title.camera'])
: props.intl.formatMessage(messages['id.verification.id.photo.title.upload'])}
? intl.formatMessage(messages['id.verification.id.photo.title.camera'])
: intl.formatMessage(messages['id.verification.id.photo.title.upload'])}
>
<div>
{idPhotoFile && !useCameraForId && (
<ImagePreview
src={idPhotoFile}
alt={props.intl.formatMessage(messages['id.verification.id.photo.preview.alt'])}
alt={intl.formatMessage(messages['id.verification.id.photo.preview.alt'])}
/>
)}
{useCameraForId ? (
<div>
<p>
{props.intl.formatMessage(messages['id.verification.id.photo.instructions.camera'])}
{intl.formatMessage(messages['id.verification.id.photo.instructions.camera'])}
</p>
<Camera onImageCapture={setIdPhotoFile} isPortrait={false} />
</div>
) : (
<div style={{ marginBottom: '1.25rem' }}>
<p data-testid="upload-text">
{props.intl.formatMessage(messages['id.verification.id.photo.instructions.upload'])}
{intl.formatMessage(messages['id.verification.id.photo.instructions.upload'])}
<SupportedMediaTypes />
</p>
<ImageFileUpload onFileChange={setIdPhotoFile} intl={props.intl} />
<ImageFileUpload onFileChange={setIdPhotoFile} />
</div>
)}
</div>
@@ -62,15 +63,11 @@ const TakeIdPhotoPanel = (props) => {
<CollapsibleImageHelp />
<div className="action-row" style={{ visibility: idPhotoFile ? 'unset' : 'hidden' }}>
<Link to={`/id-verification/${nextPanelSlug}`} className="btn btn-primary" data-testid="next-button">
{props.intl.formatMessage(messages['id.verification.next'])}
{intl.formatMessage(messages['id.verification.next'])}
</Link>
</div>
</BasePanel>
);
};
TakeIdPhotoPanel.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(TakeIdPhotoPanel);
export default TakeIdPhotoPanel;

View File

@@ -1,6 +1,6 @@
import React, { useContext, useEffect, useState } from 'react';
import { useContext, useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import { useNextPanelSlug } from '../routing-utilities';
import BasePanel from './BasePanel';
@@ -10,11 +10,12 @@ import IdVerificationContext from '../IdVerificationContext';
import messages from '../IdVerification.messages';
const TakePortraitPhotoPanel = (props) => {
const TakePortraitPhotoPanel = () => {
const panelSlug = 'take-portrait-photo';
const nextPanelSlug = useNextPanelSlug(panelSlug);
const { setFacePhotoFile, facePhotoFile } = useContext(IdVerificationContext);
const [mounted, setMounted] = useState(false);
const intl = useIntl();
useEffect(() => {
// This prevents focus switching to the heading when taking a photo
@@ -25,26 +26,22 @@ const TakePortraitPhotoPanel = (props) => {
<BasePanel
name={panelSlug}
focusOnMount={!mounted}
title={props.intl.formatMessage(messages['id.verification.portrait.photo.title.camera'])}
title={intl.formatMessage(messages['id.verification.portrait.photo.title.camera'])}
>
<div>
<p>
{props.intl.formatMessage(messages['id.verification.portrait.photo.instructions.camera'])}
{intl.formatMessage(messages['id.verification.portrait.photo.instructions.camera'])}
</p>
<Camera onImageCapture={setFacePhotoFile} isPortrait />
</div>
<CameraHelp isPortrait />
<div className="action-row" style={{ visibility: facePhotoFile ? 'unset' : 'hidden' }}>
<Link to={`/id-verification/${nextPanelSlug}`} className="btn btn-primary" data-testid="next-button">
{props.intl.formatMessage(messages['id.verification.next'])}
{intl.formatMessage(messages['id.verification.next'])}
</Link>
</div>
</BasePanel>
);
};
TakePortraitPhotoPanel.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(TakePortraitPhotoPanel);
export default TakePortraitPhotoPanel;

View File

@@ -1,19 +1,21 @@
import PropTypes from 'prop-types';
import React from 'react';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import messages from '../IdVerification.messages';
export const UnsupportedCameraDirectionsPanel = (props) => (
<>
{props.browserName === 'Chrome' && <span>{props.intl.formatMessage(messages['id.verification.camera.access.failure.unsupported.chrome.explanation'])}</span>}
<span> </span>
<span>{props.intl.formatMessage(messages['id.verification.camera.access.failure.unsupported.instructions'])}</span>
</>
);
export const UnsupportedCameraDirectionsPanel = (props) => {
const intl = useIntl();
return (
<>
{props.browserName === 'Chrome' && <span>{intl.formatMessage(messages['id.verification.camera.access.failure.unsupported.chrome.explanation'])}</span>}
<span> </span>
<span>{intl.formatMessage(messages['id.verification.camera.access.failure.unsupported.instructions'])}</span>
</>
);
};
UnsupportedCameraDirectionsPanel.propTypes = {
intl: intlShape.isRequired,
browserName: PropTypes.string.isRequired,
};
export default injectIntl(UnsupportedCameraDirectionsPanel);
export default UnsupportedCameraDirectionsPanel;

View File

@@ -4,16 +4,13 @@ import {
render, cleanup, act, screen,
} from '@testing-library/react';
import '@edx/frontend-platform/analytics';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { ERROR_REASONS } from '../IdVerificationContext';
import AccessBlocked from '../AccessBlocked';
const IntlAccessBlocked = injectIntl(AccessBlocked);
describe('AccessBlocked', () => {
const defaultProps = {
intl: {},
error: '',
};
@@ -27,7 +24,7 @@ describe('AccessBlocked', () => {
await act(async () => render((
<Router>
<IntlProvider locale="en">
<IntlAccessBlocked {...defaultProps} />
<AccessBlocked {...defaultProps} />
</IntlProvider>
</Router>
)));
@@ -43,7 +40,7 @@ describe('AccessBlocked', () => {
await act(async () => render((
<Router>
<IntlProvider locale="en">
<IntlAccessBlocked {...defaultProps} />
<AccessBlocked {...defaultProps} />
</IntlProvider>
</Router>
)));
@@ -59,7 +56,7 @@ describe('AccessBlocked', () => {
await act(async () => render((
<Router>
<IntlProvider locale="en">
<IntlAccessBlocked {...defaultProps} />
<AccessBlocked {...defaultProps} />
</IntlProvider>
</Router>
)));

View File

@@ -5,6 +5,7 @@ import {
render, cleanup, screen, act, fireEvent,
} from '@testing-library/react';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import CameraPhoto from 'jslib-html5-camera-photo';
// eslint-disable-next-line import/no-unresolved
import * as blazeface from '@tensorflow-models/blazeface';
import * as analytics from '@edx/frontend-platform/analytics';
@@ -181,4 +182,99 @@ describe('SubmittedPanel', () => {
await fireEvent.click(checkbox);
expect(analytics.sendTrackEvent).toHaveBeenCalledWith('edx.id_verification.id_photo.face_detection_disabled');
});
describe('Camera getSizeFactor method', () => {
let mockGetDataUri;
beforeEach(() => {
jest.clearAllMocks();
mockGetDataUri = jest.fn().mockReturnValue('data:image/jpeg;base64,test');
});
it('scales down large resolutions to stay under 10MB limit', async () => {
const currentSettings = { width: 4000, height: 3000 };
CameraPhoto.mockImplementation(() => ({
startCamera: jest.fn(),
stopCamera: jest.fn(),
getDataUri: mockGetDataUri,
getCameraSettings: jest.fn().mockReturnValue(currentSettings),
}));
await act(async () => render((
<Router>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<Camera {...defaultProps} />
</IdVerificationContext.Provider>
</IntlProvider>
</Router>
)));
const button = await screen.findByRole('button', { name: /take photo/i });
fireEvent.click(button);
// For large resolution: size = 4000 * 3000 * 3 = 36,000,000 bytes
// Ratio = 9,999,999 / 36,000,000 ≈ 0.278
expect(mockGetDataUri).toHaveBeenCalledWith(expect.objectContaining({
sizeFactor: expect.closeTo(0.278, 2),
}));
});
it('scales up 640x480 resolution to improve quality', async () => {
const currentSettings = { width: 640, height: 480 };
CameraPhoto.mockImplementation(() => ({
startCamera: jest.fn(),
stopCamera: jest.fn(),
getDataUri: mockGetDataUri,
getCameraSettings: jest.fn().mockReturnValue(currentSettings),
}));
await act(async () => render((
<Router>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<Camera {...defaultProps} />
</IdVerificationContext.Provider>
</IntlProvider>
</Router>
)));
const button = await screen.findByRole('button', { name: /take photo/i });
fireEvent.click(button);
expect(mockGetDataUri).toHaveBeenCalledWith(expect.objectContaining({
sizeFactor: 2,
}));
});
it('maintains original size for medium resolutions', async () => {
const currentSettings = { width: 1280, height: 720 };
CameraPhoto.mockImplementation(() => ({
startCamera: jest.fn(),
stopCamera: jest.fn(),
getDataUri: mockGetDataUri,
getCameraSettings: jest.fn().mockReturnValue(currentSettings),
}));
await act(async () => render((
<Router>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<Camera {...defaultProps} />
</IdVerificationContext.Provider>
</IntlProvider>
</Router>
)));
const button = await screen.findByRole('button', { name: /take photo/i });
fireEvent.click(button);
expect(mockGetDataUri).toHaveBeenCalledWith(expect.objectContaining({
sizeFactor: 1,
}));
});
});
});

View File

@@ -1,9 +1,8 @@
import React from 'react';
import { BrowserRouter as Router } from 'react-router-dom';
import {
render, cleanup, screen, act,
} from '@testing-library/react';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import * as analytics from '@edx/frontend-platform/analytics';
import IdVerificationContext from '../IdVerificationContext';
import CollapsibleImageHelp from '../CollapsibleImageHelp';
@@ -17,11 +16,7 @@ analytics.sendTrackEvent = jest.fn();
window.HTMLMediaElement.prototype.play = () => {};
const IntlCollapsible = injectIntl(CollapsibleImageHelp);
describe('CollapsibleImageHelpPanel', () => {
const defaultProps = { intl: {} };
const contextValue = {
useCameraForId: true,
setUseCameraForId: jest.fn(),
@@ -36,7 +31,7 @@ describe('CollapsibleImageHelpPanel', () => {
<Router>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlCollapsible {...defaultProps} />
<CollapsibleImageHelp />
</IdVerificationContext.Provider>
</IntlProvider>
</Router>
@@ -56,7 +51,7 @@ describe('CollapsibleImageHelpPanel', () => {
<Router>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlCollapsible {...defaultProps} />
<CollapsibleImageHelp />
</IdVerificationContext.Provider>
</IntlProvider>
</Router>

View File

@@ -1,13 +1,12 @@
/* eslint-disable react/jsx-no-useless-fragment */
import React from 'react';
import { Provider } from 'react-redux';
import { MemoryRouter as Router } from 'react-router-dom';
import configureStore from 'redux-mock-store';
import {
render, act, screen, fireEvent,
} from '@testing-library/react';
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
import IdVerificationPage from '../IdVerificationPage';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import IdVerificationPageSlot from '../../plugin-slots/IdVerificationPageSlot';
import * as selectors from '../data/selectors';
jest.mock('../data/selectors', () => jest.fn().mockImplementation(() => ({ idVerificationSelector: () => ({}) })));
@@ -47,22 +46,18 @@ jest.mock('../panels/SubmittedPanel', () => function SubmittedPanelMock() {
return <></>;
});
const IntlIdVerificationPage = injectIntl(IdVerificationPage);
const mockStore = configureStore();
describe('IdVerificationPage', () => {
selectors.mockClear();
jest.spyOn(Storage.prototype, 'setItem');
const store = mockStore();
const props = {
intl: {},
};
it('decodes and stores course_id', async () => {
await act(async () => render((
<Router initialEntries={[`/?course_id=${encodeURIComponent('course-v1:edX+DemoX+Demo_Course')}`]}>
<IntlProvider locale="en">
<Provider store={store}>
<IntlIdVerificationPage {...props} />
<IdVerificationPageSlot />
</Provider>
</IntlProvider>
</Router>
@@ -78,7 +73,7 @@ describe('IdVerificationPage', () => {
<Router initialEntries={['/?next=dashboard']}>
<IntlProvider locale="en">
<Provider store={store}>
<IntlIdVerificationPage {...props} />
<IdVerificationPageSlot />
</Provider>
</IntlProvider>
</Router>
@@ -93,7 +88,7 @@ describe('IdVerificationPage', () => {
<Router initialEntries={['/?next=dashboard']}>
<IntlProvider locale="en">
<Provider store={store}>
<IntlIdVerificationPage {...props} />
<IdVerificationPageSlot />
</Provider>
</IntlProvider>
</Router>
@@ -107,7 +102,7 @@ describe('IdVerificationPage', () => {
<Router initialEntries={['/?next=dashboard']}>
<IntlProvider locale="en">
<Provider store={store}>
<IntlIdVerificationPage {...props} />
<IdVerificationPageSlot />
</Provider>
</IntlProvider>
</Router>

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