Compare commits

...

120 Commits

Author SHA1 Message Date
morenol
8602561e55 fix: Update frontend-build (#340)
Update frontend-platform
2020-11-10 17:18:02 -05:00
Renovate Bot
b310574f18 fix(deps): update font awesome 2020-11-10 02:00:05 +00:00
Renovate Bot
73badaf916 fix(deps): update dependency universal-cookie to v4.0.4 2020-11-10 00:58:25 +00:00
Renovate Bot
f02cc43078 chore(deps): update dependency enzyme-adapter-react-16 to v1.15.5 2020-11-09 23:36:50 +00:00
edX Transifex Bot
bd5c2343be fix(i18n): update translations 2020-11-08 16:03:56 -05:00
alangsto
6349d487e4 Merge pull request #343 from edx/alangsto/camera_permissions
Ensure that first stream is stopped
2020-11-05 14:40:40 -05:00
Alie Langston
9c6137e668 testing to see if we need to request front/back camera
stopping initial stream
2020-11-05 14:30:39 -05:00
alangsto
2cf01270d7 Merge pull request #342 from edx/alangsto/fix_camera_access
Remove browser based resolution
2020-11-04 13:15:26 -05:00
Alie Langston
403df8926d removed browser based resolution, and instead optimized photo for all browsers
completed comment
2020-11-04 13:01:14 -05:00
Bianca Severino
4bd96c70af Merge pull request #341 from edx/bseverino/name-change-fix
Fix onSubmit for IDV name change form
2020-10-27 12:32:30 -04:00
Bianca Severino
d2a835f560 Fix submit functionality on name change form 2020-10-27 12:13:47 -04:00
Bianca Severino
d0b5d54d0a Merge pull request #339 from edx/bseverino/id-resolution
Increase camera resolution to 1080p for ID verification
2020-10-20 14:17:51 -04:00
Bianca Severino
26299eed65 Increase camera resolution to 1080p 2020-10-20 13:53:01 -04:00
adeelehsan
648bea8d84 Merge pull request #338 from edx/aehsan/suppressred_pii_for_accounts
suppressed pii for account
2020-10-09 01:59:13 +05:00
adeelehsan
7409f02056 suppressed pii for account 2020-10-09 00:54:07 +05:00
alangsto
f5dd409816 Merge pull request #337 from edx/alangsto/add_tracking_events
Add tracking events for face detection
2020-10-01 15:32:36 -04:00
Alie Langston
6ddac11dc0 added tracking events
added testing
2020-10-01 14:19:07 -04:00
adeelehsan
7019aea4fb Merge pull request #333 from edx/aehsan/van-23/suppressed_pii_for_accounts
suppressed pii for hot jar
2020-09-25 17:33:37 +05:00
adeelehsan
5424434599 suppressed pii for hot jar
VAN-23
2020-09-25 17:22:13 +05:00
edX Transifex Bot
8ca9dc78a9 fix(i18n): update translations 2020-09-20 17:06:06 -04:00
alangsto
30e25b96bb Merge pull request #332 from edx/alangsto/add_realtime_feedback
Add realtime feedback for screenreaders during face detection
2020-09-16 15:31:00 -04:00
Alie Langston
1d01abc7da added feedback for screenreaders
moved settings state back to original

fixed status updates

updated message for more clarity

renamed variables for clarity

added comment

fixed variables

fixed variable again

decreased delay between feedbacks

updated comment
2020-09-16 15:22:04 -04:00
alangsto
917152df22 Merge pull request #330 from edx/alangsto/remove_survey
Removed SurveyMonkey survey
2020-09-15 16:35:45 -04:00
Alie Langston
961c0feb78 removed SurveyMonkey survey 2020-09-15 16:04:46 -04:00
edX Transifex Bot
7d57d86729 fix(i18n): update translations 2020-09-13 17:04:11 -04:00
alangsto
34c5de1340 Merge pull request #324 from edx/alangsto/add_idv_survey
Added SurveyMonkey Survey to IDV submitted page
2020-09-10 15:42:59 -04:00
Alie Langston
81d604d046 adding second round of survey to IDV summary page 2020-09-10 15:22:21 -04:00
Michael Roytman
e6f7e83cf5 Merge pull request #322 from edx/mroytman/use-better-camera-mobile
use 'environment' facing mode when using the camera in the ID photo c…
2020-09-10 14:45:30 -04:00
Michael Roytman
a970e17070 use 'environment' facing mode when using the camera in the ID photo context; this will open the rear facing camera on mobile 2020-09-10 14:33:42 -04:00
alangsto
f471ae0aa7 Merge pull request #323 from edx/alangsto/add_error_messaging
Added error messaging for image upload and idv submission
2020-09-10 14:29:20 -04:00
Alie Langston
b9efe6faee added error messaging for image upload and idv submission
updated testing

wrapped button click

moved maximum file size to a const variable
2020-09-10 14:23:35 -04:00
edX Transifex Bot
2dbccec1f1 fix(i18n): update translations 2020-09-06 17:03:58 -04:00
Michael Roytman
9f38b975d9 Merge pull request #318 from edx/mroytman/fix-iOS-camera-bug-popout
add playsInline attribute to video element to ensure it does not pop out into full screen video on iOS Safari
2020-09-04 14:54:29 -04:00
Michael Roytman
ae355cefcf add playsInline attribute to video element to ensure it does not pop out into full screen video on iOS Safari 2020-09-04 14:03:29 -04:00
alangsto
d63dfc929f Merge pull request #317 from edx/alangsto/fix_camera_resolution
Added logic for adjusting image resolution
2020-09-03 14:11:13 -04:00
Alie Langston
64be9edeac adjusted size factor based on camera resolution
added additional check so that tests pass

updates for requested changes
2020-09-03 14:05:36 -04:00
alangsto
5f4f82eae1 Merge pull request #315 from edx/alangsto/blazeface_fix
Fix for blazeface/camera issue
2020-09-02 10:43:40 -04:00
Alie Langston
c8c7352549 updated camera and canvas ratio to match, and updated ranges for landmarks 2020-09-02 10:34:23 -04:00
Renovate Bot
88206e4282 fix(deps): update dependency @tensorflow/tfjs-core to v1.6.1 2020-09-01 22:56:01 +00:00
Renovate Bot
d8e23b1a02 fix(deps): update dependency @tensorflow/tfjs-converter to v1.6.1 2020-09-01 21:58:04 +00:00
alangsto
5db21d2483 Merge pull request #302 from edx/alangsto/add_object_tracking
added object tracking
2020-09-01 16:48:39 -04:00
Alie Langston
526d6114f2 added object tracking
moved load of library

updated test

removed async

trying to retest

Retesting

added test back

fixed errors due to next button

removed try catch so errors occur

added try catch back

added in ignore

readded libraries

stops detection when photo is taken, stops erroring issue

added help text

added spinner and better mocked blazeface

moved img element back to correct place

updated for requested changes

updates for requested changes

added timeout for test

updated blazeface to pull from forked repo, and added changes based on accessibility feedback
2020-09-01 15:54:20 -04:00
Renovate Bot
0cc38e2dc6 chore(deps): update dependency enzyme-adapter-react-16 to v1.15.4 2020-08-31 06:47:57 +00:00
edX Transifex Bot
380ca7c816 fix(i18n): update translations 2020-08-30 17:03:45 -04:00
Justin Hynes
bcb20234ab Merge pull request #296 from edx/jhynes/microba-534_demographics-i18n
MICROBA-534 | Remove hardcoded Demographics options, pull choices from Demographics API
2020-08-28 07:43:14 -04:00
Justin Hynes
87ff50ace8 MICROBA-534 | Remove hardcoded Demographics options, pull choices from Demographics API
[MICROBA-534]
- Pull Demographics field choices from Demographics API to support I18N/L10N effors so that we don't need duplicated choices for the fields in two places (backend and frontend)
- Refactor error detection code in DemographicsSection component
2020-08-27 14:31:06 -04:00
alangsto
b409aff6b4 Merge pull request #301 from edx/alangsto/fix_focus_on_permissions_panel
Fixed HTML element focus on camera permissions panel
2020-08-25 12:50:51 -04:00
Michael Roytman
2b67d037bf Merge pull request #293 from edx/mroytman/MST-361-stop-camera-after-submit
stop camera once user successfully submits photos to IDVerification s…
2020-08-25 12:19:16 -04:00
Alie Langston
4137996a91 fixed focus issue
removed test id

removed space
2020-08-25 12:16:17 -04:00
Michael Roytman
1319bd6377 stop camera once user successfully submits photos to IDVerification service 2020-08-25 12:07:58 -04:00
alangsto
a579e86e98 Merge pull request #300 from edx/alangsto/add_image_upload_toggle
Added option for ID image upload
2020-08-24 15:51:18 -04:00
Alie Langston
74bb2fb45f added option for image upload
added friction to allowing users to upload id image

only shows picture within component if it has been uploaded, not if it was captured through the webcam
2020-08-24 15:34:53 -04:00
edX Transifex Bot
11dbbad20b fix(i18n): update translations 2020-08-23 17:03:33 -04:00
alangsto
a0227f1dbc Merge pull request #298 from edx/alangsto/add_instructions_for_camera_permissions
Added instructions for camera permissions
2020-08-20 10:44:17 -04:00
Alie Langston
f6a7a6063c added instructions for enabling camera
updated message

created separate component
2020-08-20 10:39:27 -04:00
Zachary Hancock
0aa02687e6 Merge pull request #297 from edx/zhancock/camera-fix
Limit camera to 720p
2020-08-19 09:02:59 -04:00
Zach Hancock
303f6a5d3f limit camera to 720p 2020-08-18 16:45:48 -04:00
edX Transifex Bot
b262f42c8d fix(i18n): update translations 2020-08-16 17:07:29 -04:00
Bianca Severino
b92794b72c Merge pull request #294 from edx/bseverino/idv-tests
Add tests for new IDV functionality
2020-08-14 12:35:34 -04:00
Bianca Severino
3f754fa114 Add tests for new IDV functionality 2020-08-14 11:39:26 -04:00
Thomas Tracy
bf274e5186 Fix extra call to LMS when PATCHing demographics (#291)
We were making an extra call to the LMS when making a PATCH to the
demographics service which we don't need to do.

The keys for demographics fields we not being removed from the
accountSettings object properly, which made them be seen as changes,
which triggered the call to the LMS. I've added a constant
DEMOGRAPHICS_FIELDS that keys out those fields, removing the call.
2020-08-13 15:00:24 -04:00
Bianca Severino
be9cf70c5c Merge pull request #292 from edx/bseverino/idv-improvements
[MST-353] IDV a11y and UX improvements
2020-08-13 10:01:44 -04:00
Bianca Severino
c00ea15920 IDV a11y and UX improvements 2020-08-12 16:10:30 -04:00
Bianca Severino
5a8bd309e7 Merge pull request #289 from edx/bseverino/idv-improvements
Add i18n and UX improvements to IDV
2020-08-11 11:43:23 -04:00
Bianca Severino
d83ea54272 Add i18n and UX improvements to IDV 2020-08-11 10:19:21 -04:00
Justin Hynes
eae18d9c63 Merge pull request #290 from edx/jhynes/microba-405_add-missing-income-option
MICROBA-405 | Add missing demographics income option
2020-08-11 08:48:23 -04:00
Justin Hynes
a18da61cec MICROBA-405 | Add missing demographics income option
[MICROBA-405]
- Add missing demographics income option
2020-08-10 15:18:43 -04:00
Renovate Bot
80d5fd2a34 chore(deps): update dependency enzyme-adapter-react-16 to v1.15.3 2020-08-08 03:41:58 +00:00
Jeff LaJoie
d4e9ba0420 Merge pull request #287 from edx/jlajoie/ENT-3302
fix: ENTERPRISE_LEARNER_PORTAL_HOSTNAME added for header updates
2020-08-06 07:25:56 -04:00
Jeff LaJoie
5add376c31 fix: ENTERPRISE_LEARNER_PORTAL_HOSTNAME added for header updates 2020-08-06 07:13:04 -04:00
Bianca Severino
559c9aa1a9 Merge pull request #284 from edx/bseverino/idv-testing
Add React Testing Library and write unit tests for IDV
2020-08-03 09:13:08 -04:00
Bianca Severino
ea8a6d29d0 Add React Testing Library and write unit tests for IDV 2020-08-03 09:05:44 -04:00
edX Transifex Bot
4689482137 fix(i18n): update translations 2020-08-02 17:06:56 -04:00
Renovate Bot
d48be79e53 Update dependency @edx/paragon to v9.1.1 2020-07-31 19:14:09 +00:00
Thomas Tracy
ed94cc68e3 Force content type header to 'application/json' (#282) 2020-07-29 15:06:36 -04:00
Bianca Severino
05740b37ff Merge pull request #281 from edx/bseverino/remove-survey
Remove SurveyMonkey widget from IDV
2020-07-29 11:07:55 -04:00
Bianca Severino
b2c8164cd1 Remove SurveyMonkey widget from IDV 2020-07-29 09:58:17 -04:00
Bianca Severino
47b369f797 Merge pull request #280 from edx/bseverino/idv-spinner
Update Paragon and add loading state + spinner to IDV summary page
2020-07-24 16:16:58 -04:00
Bianca Severino
88239f2700 Update Paragon and add loading state + spinner to IDV summary page 2020-07-24 16:07:53 -04:00
Bianca Severino
602c4b484c Merge pull request #279 from edx/bseverino/message-fix
Fixed IDV submitted message to estimate 5 days rather than 1-2 days
2020-07-24 12:16:45 -04:00
Bianca Severino
45ec573ff9 Fixed IDV cubmitted message to estimate 5 days rather than 1-2 days 2020-07-24 11:39:56 -04:00
Justin Hynes
e0befb8b60 Merge pull request #272 from edx/jhynes/microba-488_improve-error-handling
MICROBA-488 | Improve error handling in DemographicsSection
2020-07-24 08:58:43 -04:00
Justin Hynes
9f4d944670 MICROBA-488 | Improve error handling in DemographicsSection
[MICROBA-488]
- Improve error handling in DemographicsSection
2020-07-24 08:49:12 -04:00
Bianca Severino
60d26649dd Merge pull request #278 from edx/bseverino/tracking-fix
Changed sendTrackingLogEvent to sendTrackEvent
2020-07-23 19:21:04 -04:00
Bianca Severino
55f25c73ca Changed sendTrackingLogEvent to sendTrackEvent 2020-07-23 19:03:11 -04:00
Renovate Bot
29dbdf6ad0 Update dependency react-scrollspy to v3.4.3 2020-07-23 02:05:03 +00:00
Bianca Severino
5bfa834563 Merge pull request #276 from edx/bseverino/css-fix
Remove PurgeCSS
2020-07-22 16:04:52 -04:00
Bianca Severino
5e3af50e3b Remove PurgeCSS 2020-07-22 12:51:24 -04:00
Bianca Severino
e30a20b185 Merge pull request #273 from edx/bseverino/idv-analytics
Add tracking log events to IDV
2020-07-22 11:14:46 -04:00
Renovate Bot
2743e05890 Update dependency codecov to v3.7.2 2020-07-22 03:26:31 +00:00
Olivia Ruiz-Knott
71c0563e3a MICROBA-487 Fix legal text spacing on demographics section (#274) 2020-07-21 15:58:55 -04:00
Bianca Severino
5d8d327a48 Added tracking log events to IDV 2020-07-21 13:57:09 -04:00
Albert (AJ) St. Aubin
15ba7c087e Merge pull request #266 from edx/aj/MICROBA-484
[MICROBA-484] Scroll to section based on URL
2020-07-21 13:45:27 -04:00
Olivia Ruiz-Knott
6df7ad243b MICROBA-463 Collapse phone number field when coaching toggled on (#271) 2020-07-21 13:09:45 -04:00
Bianca Severino
594d3ff9f1 Merge pull request #264 from edx/bseverino/hide-image-upload
ID Verification: Hide image upload component
2020-07-21 10:16:09 -04:00
Renovate Bot
7bb9d09dae fix(deps): update dependency @fortawesome/fontawesome-svg-core to v1.2.30 2020-07-21 13:48:44 +00:00
dependabot[bot]
0e6233b693 chore(deps-dev): bump codecov from 3.6.5 to 3.7.1 (#269)
Bumps [codecov](https://github.com/codecov/codecov-node) from 3.6.5 to 3.7.1.
- [Release notes](https://github.com/codecov/codecov-node/releases)
- [Commits](https://github.com/codecov/codecov-node/compare/v3.6.5...v3.7.1)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-07-21 09:24:35 -04:00
Renovate Bot
22087d2d2c fix(deps): update dependency @edx/frontend-component-footer to v10.0.11 2020-07-20 19:58:22 +00:00
Albert (AJ) St. Aubin
64798cdc80 Remove jumpy scrolling 2020-07-20 15:12:51 -04:00
Albert (AJ) St. Aubin
f53ba967e5 Simplification 2020-07-20 14:28:23 -04:00
Albert (AJ) St. Aubin
2deb47d542 [MICROBA-484] Scroll to section based on URL
Added in a check for a locationHash and loaded state to
scroll to the section in the URL so that the page would load to a Nav
Section after loading data.
2020-07-20 14:02:48 -04:00
edX Transifex Bot
1b9dd3bdf5 fix(i18n): update translations 2020-07-19 17:06:34 -04:00
Bianca Severino
fd3c8ede6d Hide image upload flow 2020-07-17 14:11:58 -04:00
Justin Hynes
c1fdace72d Merge pull request #261 from edx/jhynes/microba-474_determine-demographics-visibility
MICROBA-474 | Gate Demographics questions and link via LMS API call
2020-07-16 08:27:03 -04:00
Justin Hynes
4ca4d55796 MICROBA-474 | Gate Demographics questions and link via LMS API call
[MICROBA-474]
- Add logic to show (or hide) the "Optional Information" (Demographics questions) section on the Account settings page via call to new LMS API
2020-07-15 12:01:52 -04:00
Bianca Severino
2da606bf6f Merge pull request #260 from edx/bseverino/idv-course-key
Persist courseRunKey in ID verification flow
2020-07-13 16:45:19 -04:00
Bianca Severino
997a3c0b98 Add ability to store and retrieve course run key
Add course run key to POST request
Change text based on return location
Minor style fixes
2020-07-13 14:41:35 -04:00
Justin Hynes
f627257a1c Merge pull request #258 from edx/jhynes/microba-471_add-self-employed-choice
MICROBA-471 | Add 'self-employed' option to work status options
2020-07-13 08:32:31 -04:00
edX Transifex Bot
9aa2a816b4 fix(i18n): update translations 2020-07-12 17:06:13 -04:00
Justin Hynes
fc54dd528f MICROBA-471 | Add 'self-employed' option to work status options
Add support for self-employed option added to the work status options on the UserDemographics model.
2020-07-10 15:50:06 -04:00
Albert (AJ) St. Aubin
53fc1b325c Merge pull request #257 from edx/aj/MICROBA-445
Adding a link to the legal copy for demographics
2020-07-09 13:06:22 -04:00
Albert (AJ) St. Aubin
48b02cd2de Adding a link to the legal copy for demographics 2020-07-09 12:45:12 -04:00
edX Transifex Bot
490274b2ed fix(i18n): update translations 2020-07-05 17:06:23 -04:00
Renovate Bot
10dc9aabde Update dependency @edx/frontend-component-footer to v10.0.10 2020-07-04 11:16:45 +00:00
Bianca Severino
74ec75781e Merge pull request #253 from edx/bseverino/surveymonkey
Add SurveyMonkey widget to IDV submitted page
2020-07-02 10:52:47 -04:00
Justin Hynes
c428d3044f Merge pull request #254 from edx/jhynes/microba-465_fix-demographics-alert-behavior
MICROBA-465 | Fix Demographics alert behavior
2020-07-01 12:46:19 -04:00
Justin Hynes
d9777fe48e MICROBA-465 | Fix alert behavior on Accounts to only display on Demographics errors
[MICROBA-465]
- Fix Demographics alert behavior on Accounts page to only display on Demographics errors
2020-07-01 12:07:28 -04:00
Bianca Severino
6b7ab05dd5 Add SurveyMonkey widget to IDV submitted page 2020-07-01 11:07:59 -04:00
Justin Hynes
ce79cd7f5a Merge pull request #251 from edx/jhynes/microba-309_demographics
MICROBA-309 | DemographicsSection component tests and error handling
2020-07-01 08:29:26 -04:00
Justin Hynes
b8ab0a2150 MICROBA-309 | DemographicsSection component tests and error handling
[MICROBA-309]
- Fix defect in ethnicityFieldDisplay function. Check to make sure key/field exists before accessing
- Add additional error handling. Add ability to show an Alert to the user if a call to the Demographics API fails
- Add tests
2020-07-01 08:11:47 -04:00
63 changed files with 24922 additions and 6551 deletions

1
.env
View File

@@ -16,3 +16,4 @@ SEGMENT_KEY=null
SITE_NAME=null
SUPPORT_URL=null
USER_INFO_COOKIE_NAME=null
ENTERPRISE_LEARNER_PORTAL_HOSTNAME=null

View File

@@ -7,7 +7,7 @@ LANGUAGE_PREFERENCE_COOKIE_NAME='openedx-language-preference'
LMS_BASE_URL='http://localhost:18000'
LOGIN_URL='http://localhost:18000/login'
LOGOUT_URL='http://localhost:18000/logout'
MARKETING_SITE_BASE_URL='http://localhost:18000'
MARKETING_SITE_BASE_URL='http://localhost:5335'
DEMOGRAPHICS_BASE_URL='http://localhost:18360'
NODE_ENV='development'
ORDER_HISTORY_URL='localhost:1996/orders'
@@ -20,3 +20,4 @@ USER_INFO_COOKIE_NAME='edx-user-info'
# Temporary, Remove this once we are ready to release the feature.
COACHING_ENABLED=true
ENABLE_DEMOGRAPHICS_COLLECTION=true
ENTERPRISE_LEARNER_PORTAL_HOSTNAME=null='http://localhost:8080'

View File

@@ -7,7 +7,7 @@ LANGUAGE_PREFERENCE_COOKIE_NAME='openedx-language-preference'
LMS_BASE_URL='http://localhost:18000'
LOGIN_URL='http://localhost:18000/login'
LOGOUT_URL='http://localhost:18000/logout'
MARKETING_SITE_BASE_URL='http://localhost:18000'
MARKETING_SITE_BASE_URL='http://localhost:5335'
DEMOGRAPHICS_BASE_URL='http://localhost:18360'
NODE_ENV=null
ORDER_HISTORY_URL='localhost:1996/orders'

View File

@@ -41,11 +41,6 @@ This MFE is configured via node environment variables supplied at build time. Se
For more information see the document: `Micro-frontend applications in Open
edX <https://github.com/edx/edx-developer-docs/blob/5191e800bf16cf42f25c58c58f983bdaf7f9305d/docs/micro-frontends-in-open-edx.rst>`__.
Notes
-----
The production Webpack configuration for this repo uses `Purgecss <https://www.purgecss.com/>`__ to remove unused CSS from the production css file. In ``webpack.prod.config.js`` the Purgecss plugin is configured to scan directories to determine what css selectors should remain. Currently the src/ directory is scanned along with all ``@edx/frontend-component*`` node modules and ``@edx/paragon``. **If you add and use a component in this repo that relies on HTML classes or ids for styling you must add it to the Purgecss configuration or it will be unstyled in the production build.**
.. |Build Status| image:: https://api.travis-ci.org/edx/frontend-app-account.svg?branch=master
:target: https://travis-ci.org/edx/frontend-app-account
.. |Codecov| image:: https://img.shields.io/codecov/c/github/edx/frontend-app-account

22823
package-lock.json generated

File diff suppressed because it is too large Load Diff

29
package.json Executable file → Normal file
View File

@@ -29,16 +29,20 @@
"ie 11"
],
"dependencies": {
"@edx/frontend-component-footer": "10.0.9",
"@edx/frontend-component-footer": "10.0.11",
"@edx/frontend-component-header": "2.0.5",
"@edx/frontend-platform": "1.1.14",
"@edx/paragon": "7.1.5",
"@fortawesome/fontawesome-svg-core": "1.2.29",
"@edx/frontend-platform": "1.6.1",
"@edx/paragon": "9.1.1",
"@fortawesome/fontawesome-svg-core": "1.2.32",
"@fortawesome/free-brands-svg-icons": "5.8.2",
"@fortawesome/free-regular-svg-icons": "5.7.2",
"@fortawesome/free-solid-svg-icons": "5.8.2",
"@fortawesome/react-fontawesome": "0.1.11",
"@fortawesome/react-fontawesome": "0.1.12",
"@tensorflow-models/blazeface": "git+https://github.com/alangsto/blazeface.git",
"@tensorflow/tfjs-converter": "1.6.1",
"@tensorflow/tfjs-core": "1.6.1",
"babel-polyfill": "6.26.0",
"bowser": "^2.10.0",
"classnames": "2.2.6",
"font-awesome": "4.7.0",
"form-urlencoded": "4.0.1",
@@ -65,7 +69,7 @@
"react-router": "5.1.2",
"react-router-dom": "5.1.2",
"react-router-hash-link": "1.2.2",
"react-scrollspy": "3.4.2",
"react-scrollspy": "3.4.3",
"react-transition-group": "4.3.0",
"redux": "4.0.5",
"redux-devtools-extension": "2.13.8",
@@ -73,17 +77,18 @@
"redux-saga": "1.1.3",
"redux-thunk": "2.3.0",
"reselect": "4.0.0",
"universal-cookie": "4.0.3"
"universal-cookie": "4.0.4"
},
"devDependencies": {
"@edx/frontend-build": "2.0.6",
"codecov": "3.6.5",
"@edx/frontend-build": "5.3.2",
"@testing-library/jest-dom": "^5.11.2",
"@testing-library/react": "^10.4.7",
"codecov": "3.7.2",
"enzyme": "3.10.0",
"enzyme-adapter-react-16": "1.15.2",
"enzyme-adapter-react-16": "1.15.5",
"es-check": "5.0.0",
"glob": "7.1.6",
"husky": "3.0.9",
"purgecss-webpack-plugin": "1.6.0",
"jest": "^26.1.0",
"react-test-renderer": "16.8.6",
"reactifex": "1.1.1",
"redux-mock-store": "1.5.4"

View File

@@ -4,7 +4,7 @@
<title>Account | edX</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
<link rel="shortcut icon" href="<%=webpackConfig.output.publicPath%>favicon.ico" type="image/x-icon" />
</head>
<body>
<div id="root"></div>

View File

@@ -38,6 +38,7 @@ import { fetchSiteLanguages } from './site-language';
import CoachingToggle from './coaching/CoachingToggle';
import DemographicsSection from './demographics/DemographicsSection';
class AccountSettingsPage extends React.Component {
constructor(props, context) {
super(props, context);
@@ -53,6 +54,16 @@ class AccountSettingsPage extends React.Component {
this.state = {
duplicateTpaProvider,
};
this.navLinkRefs = {
'#basic-information': React.createRef(),
'#profile-information': React.createRef(),
'#demographics-information': React.createRef(),
'#social-media': React.createRef(),
'#site-preferences': React.createRef(),
'#linked-accounts': React.createRef(),
'#delete-account': React.createRef(),
};
}
componentDidMount() {
@@ -65,6 +76,19 @@ class AccountSettingsPage extends React.Component {
});
}
componentDidUpdate(prevProps) {
if (prevProps.loading && !prevProps.loaded && this.props.loaded) {
const locationHash = global.location.hash;
// Check for the locationHash in the URL and then scroll to it if it is in the
// NavLinks list
if (typeof locationHash !== 'string')
return;
if (Object.keys(this.navLinkRefs).includes(locationHash) && this.navLinkRefs[locationHash].current) {
window.scrollTo(0, this.navLinkRefs[locationHash].current.offsetTop)
}
}
}
// NOTE: We need 'locale' for the memoization in getLocalizedTimeZoneOptions. Don't remove it!
// eslint-disable-next-line no-unused-vars
getLocalizedTimeZoneOptions = memoize((timeZoneOptions, countryTimeZoneOptions, locale) => {
@@ -207,6 +231,17 @@ 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']}/>
);
} else {
return null;
}
}
renderContent() {
const editableFieldProps = {
onChange: this.handleEditableFieldChange,
@@ -236,7 +271,7 @@ class AccountSettingsPage extends React.Component {
return (
<React.Fragment>
<div className="account-section" id="basic-information">
<div className="account-section" id="basic-information" ref={this.navLinkRefs['#basic-information']}>
<h2 className="section-heading">
{this.props.intl.formatMessage(messages['account.settings.section.account.information'])}
</h2>
@@ -323,7 +358,7 @@ class AccountSettingsPage extends React.Component {
}
</div>
<div className="account-section" id="profile-information">
<div className="account-section" id="profile-information" ref={this.navLinkRefs['#profile-information']}>
<h2 className="section-heading">
{this.props.intl.formatMessage(messages['account.settings.section.profile.information'])}
</h2>
@@ -364,9 +399,7 @@ class AccountSettingsPage extends React.Component {
/>
}
</div>
{getConfig().ENABLE_DEMOGRAPHICS_COLLECTION &&
<DemographicsSection />
}
{getConfig().ENABLE_DEMOGRAPHICS_COLLECTION && this.renderDemographicsSection()}
<div className="account-section" id="social-media">
<h2 className="section-heading">
{this.props.intl.formatMessage(messages['account.settings.section.social.media'])}
@@ -399,7 +432,7 @@ class AccountSettingsPage extends React.Component {
/>
</div>
<div className="account-section" id="site-preferences">
<div className="account-section" id="site-preferences" ref={this.navLinkRefs['#site-preferences']}>
<h2 className="section-heading">
{this.props.intl.formatMessage(messages['account.settings.section.site.preferences'])}
</h2>
@@ -430,13 +463,13 @@ class AccountSettingsPage extends React.Component {
/>
</div>
<div className="account-section" id="linked-accounts">
<div className="account-section" id="linked-accounts" ref={this.navLinkRefs['#linked-accounts']}>
<h2 className="section-heading">{this.props.intl.formatMessage(messages['account.settings.section.linked.accounts'])}</h2>
<p>{this.props.intl.formatMessage(messages['account.settings.section.linked.accounts.description'])}</p>
<ThirdPartyAuth />
</div>
<div className="account-section" id="delete-account">
<div className="account-section" id="delete-account" ref={this.navLinkRefs['#delete-account']}>
<DeleteAccount
isVerifiedAccount={this.props.isActive}
hasLinkedTPA={hasLinkedTPA}
@@ -479,7 +512,9 @@ class AccountSettingsPage extends React.Component {
<div>
<div className="row">
<div className="col-md-3">
<JumpNav />
<JumpNav
displayDemographicsLink={this.props.formValues.shouldDisplayDemographicsSection}
/>
</div>
<div className="col-md-9">
{loading ? this.renderLoading() : null}

View File

@@ -106,6 +106,7 @@ function EditableField(props) {
>
<label className="h6 d-block" htmlFor={id}>{label}</label>
<Input
data-hj-suppress
name={name}
id={id}
type={type}
@@ -155,7 +156,7 @@ function EditableField(props) {
</Button>
) : null}
</div>
<p>{renderValue(value)}</p>
<p data-hj-suppress>{renderValue(value)}</p>
<p className="small text-muted mt-n2">{renderConfirmationMessage() || helpText}</p>
</div>
),

View File

@@ -109,6 +109,7 @@ function EmailField(props) {
>
<label className="h6 d-block" htmlFor={id}>{label}</label>
<Input
data-hj-suppress
name={name}
id={id}
type="email"
@@ -156,7 +157,7 @@ function EmailField(props) {
</Button>
) : null}
</div>
<p>{renderValue()}</p>
<p data-hj-suppress>{renderValue()}</p>
{renderConfirmationMessage() || <p className="small text-muted mt-n2">{helpText}</p>}
</div>
),

View File

@@ -5,9 +5,10 @@ import Scrollspy from 'react-scrollspy';
import messages from './AccountSettingsPage.messages';
import { getConfig } from '@edx/frontend-platform';
import PropTypes from 'prop-types';
function JumpNav({ intl }) {
function JumpNav({ intl, displayDemographicsLink }) {
return (
<div className="jump-nav">
<Scrollspy
@@ -33,7 +34,7 @@ function JumpNav({ intl }) {
{intl.formatMessage(messages['account.settings.section.profile.information'])}
</NavHashLink>
</li>
{getConfig().ENABLE_DEMOGRAPHICS_COLLECTION &&
{getConfig().ENABLE_DEMOGRAPHICS_COLLECTION && displayDemographicsLink &&
<li>
<NavHashLink to="#demographics-information">
{intl.formatMessage(messages['account.settings.section.demographics.information'])}
@@ -68,7 +69,11 @@ function JumpNav({ intl }) {
JumpNav.propTypes = {
intl: intlShape.isRequired,
displayDemographicsLink: PropTypes.bool.isRequired,
};
JumpNav.defaultProps = {
displayDemographicsLink: false,
}
export default injectIntl(JumpNav);

View File

@@ -33,7 +33,7 @@ const CoachingToggle = props => (
formId: 'phone_number',
commitValues: props.phone_number,
},
]);
], 'phone_number');
}
return props.saveSettings('phone_number', props.phone_number);
}}

View File

@@ -113,91 +113,6 @@ export function getStatesList(country) {
return country && COUNTRY_STATES_MAP[country.toUpperCase()];
}
export const SELF_DESCRIBE = 'self-describe';
export const DEMOGRAPHICS_GENDER_OPTIONS = [
'',
'woman',
'man',
'non-binary',
SELF_DESCRIBE,
];
export const OTHER = 'other';
export const DEMOGRAPHICS_ETHNICITY_OPTIONS = [
'american-indian-or-alaska-native',
'asian',
'black-or-african-american',
'hispanic-latin-spanish',
'middle-eastern-or-north-african',
'native-hawaiian-or-pacific-islander',
'white',
OTHER,
];
export const DEMOGRAPHICS_INCOME_OPTIONS = [
'',
'less-than-10k',
'10k-25k',
'25k-50k',
'50k-75k',
'over-100k',
'unsure',
];
export const DEMOGRAPHICS_MILITARY_HISTORY_OPTIONS = [
'',
'never-served',
'training',
'active',
'previously-active',
];
export const DEMOGRAPHICS_EDUCATION_LEVEL_OPTIONS = [
'',
'no-high-school',
'some-high-school',
'high-school-ged-equivalent',
'some-college',
'associates',
'bachelors',
'masters',
'professional',
'doctorate',
];
export const DEMOGRAPHICS_WORK_STATUS_OPTIONS = [
'',
'full-time',
'part-time',
'not-employed-looking',
'not-employed-not-looking',
'unable',
'retired',
'other',
];
export const DEMOGRAPHICS_WORK_SECTOR_OPTIONS = [
'',
'accommodation-food',
'administrative-support-waste-remediation',
'agriculture-forestry-fishing-hunting',
'arts-entertainment-recreation',
'construction',
'educational',
'finance-insurance',
'healthcare-social',
'information',
'management',
'manufacturing',
'mining-quarry-oil-gas',
'professional-scientific-technical',
'public-admin',
'real-estate',
'retail',
'transport-warehousing',
'utilities',
'trade',
'other',
];
export const DECLINED = 'declined';
export const SELF_DESCRIBE = 'self-describe';
export const OTHER = 'other';

View File

@@ -1,6 +1,5 @@
import { createSelector, createStructuredSelector } from 'reselect';
import { siteLanguageOptionsSelector, siteLanguageListSelector } from '../site-language';
import { siteLanguageListSelector, siteLanguageOptionsSelector } from '../site-language';
export const storeName = 'accountSettings';
@@ -195,11 +194,14 @@ export const coachingConsentPageSelector = createSelector(
export const demographicsSectionSelector = createSelector(
formValuesSelector,
draftsSelector,
errorSelector,
(
formValues,
drafts,
errors,
) => ({
formValues,
drafts,
formErrors: errors,
}),
);

View File

@@ -8,7 +8,8 @@ import isEmpty from 'lodash.isempty';
import { handleRequestError, unpackFieldErrors } from './utils';
import { getThirdPartyAuthProviders } from '../third-party-auth';
import { getCoachingPreferences, patchCoachingPreferences } from '../coaching/data/service';
import { getDemographics, patchDemographics } from '../demographics/data/service';
import { getDemographics, getDemographicsOptions, patchDemographics } from '../demographics/data/service';
import { DEMOGRAPHICS_FIELDS } from '../demographics/data/utils';
const SOCIAL_PLATFORMS = [
{ id: 'twitter', key: 'social_link_twitter' },
@@ -151,9 +152,31 @@ 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;
}
/**
* A single function to GET everything considered a setting.
* Currently encapsulates Account, Preferences, Coaching, and ThirdPartyAuth
* Currently encapsulates Account, Preferences, Coaching, ThirdPartyAuth, and Demographics
*/
export async function getSettings(username, userRoles, userId) {
const results = await Promise.all([
@@ -163,7 +186,9 @@ export async function getSettings(username, userRoles, userId) {
getProfileDataManager(username, userRoles),
getTimeZones(),
getConfig().COACHING_ENABLED && getCoachingPreferences(userId),
getConfig().ENABLE_DEMOGRAPHICS_COLLECTION && shouldDisplayDemographicsQuestions(),
getConfig().ENABLE_DEMOGRAPHICS_COLLECTION && getDemographics(userId),
getConfig().ENABLE_DEMOGRAPHICS_COLLECTION && getDemographicsOptions(),
]);
return {
@@ -173,7 +198,9 @@ export async function getSettings(username, userRoles, userId) {
profileDataManager: results[3],
timeZones: results[4],
coaching: results[5],
...results[6], // demographics
shouldDisplayDemographicsSection: results[6],
...results[7], // demographics
demographicsOptions: results[8],
};
}
@@ -187,8 +214,9 @@ export async function patchSettings(username, commitValues, userId) {
// user/v1/preferences where it does update. This is the one we use.
const preferenceKeys = ['time_zone'];
const coachingKeys = ['coaching'];
const demographicsKeys = DEMOGRAPHICS_FIELDS;
const isDemographicsKey = (value, key) => key.includes('demographics');
const accountCommitValues = omit(commitValues, preferenceKeys, coachingKeys);
const accountCommitValues = omit(commitValues, preferenceKeys, coachingKeys, demographicsKeys);
const preferenceCommitValues = pick(commitValues, preferenceKeys);
const coachingCommitValues = pick(commitValues, coachingKeys);
const demographicsCommitValues = pickBy(commitValues, isDemographicsKey);

View File

@@ -29,7 +29,7 @@ export const Checkboxes = (props) => {
setSelected(newSelected);
}
// If unchecked, make sure this option is NOT in `seleted`
// If unchecked, make sure this option is NOT in `selected`
if (!value) {
setSelected(selected.filter(i => i !== option));
}

View File

@@ -1,79 +1,104 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Input } from '@edx/paragon';
import memoize from 'memoize-one';
import { demographicsSectionSelector } from '../data/selectors';
import { saveMultipleSettings, updateDraft } from '../data/actions';
import EditableField from '../EditableField';
import Checkboxes from './Checkboxes';
import messages from './DemographicsSection.messages';
import {
SELF_DESCRIBE,
DEMOGRAPHICS_GENDER_OPTIONS,
DEMOGRAPHICS_ETHNICITY_OPTIONS,
DEMOGRAPHICS_INCOME_OPTIONS,
DEMOGRAPHICS_MILITARY_HISTORY_OPTIONS,
DEMOGRAPHICS_EDUCATION_LEVEL_OPTIONS,
OTHER,
DEMOGRAPHICS_WORK_STATUS_OPTIONS,
DEMOGRAPHICS_WORK_SECTOR_OPTIONS,
DECLINED,
SELF_DESCRIBE,
} from '../data/constants';
import { getConfig } from '@edx/frontend-platform';
import {
FormattedMessage,
injectIntl,
intlShape,
} from '@edx/frontend-platform/i18n';
import { saveMultipleSettings, updateDraft } from '../data/actions';
import Alert from '../Alert';
import Checkboxes from './Checkboxes';
import EditableField from '../EditableField';
import { Input } from '@edx/paragon';
import PropTypes from 'prop-types';
import React from 'react';
import { connect } from 'react-redux';
import { demographicsSectionSelector } from '../data/selectors';
import get from 'lodash.get';
import isEmpty from 'lodash.isempty';
import memoize from 'memoize-one';
import messages from './DemographicsSection.messages';
class DemographicsSection extends React.Component {
constructor(props, context) {
super(props, context)
super(props, context);
}
getLocalizedOptions = memoize((locale) => ({
demographicsGenderOptions: DEMOGRAPHICS_GENDER_OPTIONS.map(key => ({
value: key,
label: this.props.intl.formatMessage(messages[`account.settings.field.demographics.gender.options.${key || 'empty'}`]),
})).concat(this.getDeclinedOption()),
demographicsEthnicityOptions: DEMOGRAPHICS_ETHNICITY_OPTIONS.map(key => ({
value: key,
label: this.props.intl.formatMessage(messages[`account.settings.field.demographics.ethnicity.options.${key || 'empty'}`]),
})).concat(this.getDeclinedOption()),
demographicsIncomeOptions: DEMOGRAPHICS_INCOME_OPTIONS.map(key => ({
value: key,
label: this.props.intl.formatMessage(messages[`account.settings.field.demographics.income.options.${key || 'empty'}`]),
})).concat(this.getDeclinedOption()),
demographicsMilitaryHistoryOptions: DEMOGRAPHICS_MILITARY_HISTORY_OPTIONS.map(key => ({
value: key,
label: this.props.intl.formatMessage(messages[`account.settings.field.demographics.military_history.options.${key || 'empty'}`]),
})).concat(this.getDeclinedOption()),
demographicsEducationLevelOptions: DEMOGRAPHICS_EDUCATION_LEVEL_OPTIONS.map(key => ({
value: key,
label: this.props.intl.formatMessage(messages[`account.settings.field.demographics.education_level.options.${key || 'empty'}`]),
})).concat(this.getDeclinedOption()),
demographicsWorkStatusOptions: DEMOGRAPHICS_WORK_STATUS_OPTIONS.map(key => ({
value: key,
label: this.props.intl.formatMessage(messages[`account.settings.field.demographics.work_status.options.${key || 'empty'}`]),
})).concat(this.getDeclinedOption()),
demographicsWorkSectorOptions: DEMOGRAPHICS_WORK_SECTOR_OPTIONS.map(key => ({
value: key,
label: this.props.intl.formatMessage(messages[`account.settings.field.demographics.work_sector.options.${key || 'empty'}`]),
})).concat(this.getDeclinedOption()),
/**
* 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);
}
/**
* 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]),
}];
}
// 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
}))),
}));
getDeclinedOption() {
return [{
value: DECLINED,
label: this.props.intl.formatMessage(messages[`account.settings.field.demographics.options.declined`])
}]
}
ethnicityFieldDisplay = () => {
const ethnicities = this.props.formValues.demographics_user_ethnicity;
return ethnicities.map((e) => {
if (e == DECLINED) {
return this.props.intl.formatMessage(messages[`account.settings.field.demographics.options.declined`]);
}
return this.props.intl.formatMessage(messages[`account.settings.field.demographics.ethnicity.options.${e}`])
}).join(", ")
ethnicityFieldDisplay = (demographicsEthnicityOptions) => {
if (get(this, 'props.formValues.demographics_user_ethnicity')) {
const ethnicities = this.props.formValues.demographics_user_ethnicity;
return ethnicities.map((e) => {
var matchingOption = demographicsEthnicityOptions.filter(option => option.value === e)[0];
return matchingOption && matchingOption.label;
}).join(", ")
}
}
handleEditableFieldChange = (name, value) => {
@@ -95,6 +120,32 @@ class DemographicsSection extends React.Component {
this.props.saveMultipleSettings(settingsArray, formId);
};
/**
* 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() == false) {
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>
);
} else {
return null;
}
}
render() {
const editableFieldProps = {
onChange: this.handleEditableFieldChange,
@@ -109,137 +160,151 @@ class DemographicsSection extends React.Component {
demographicsEducationLevelOptions,
demographicsWorkStatusOptions,
demographicsWorkSectorOptions,
} = this.getLocalizedOptions(this.context.locale);
} = 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" id="demographics-information">
<div className="account-section" id="demographics-information" ref={this.props.forwardRef}>
<h2 className="section-heading">
{this.props.intl.formatMessage(messages['account.settings.section.demographics.information'])}
</h2>
<EditableField
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 &&
<Input
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"
<p>
<a href={getConfig().MARKETING_SITE_BASE_URL + '/demographics'} target="_blank">
{this.props.intl.formatMessage(messages['account.settings.section.demographics.why'])}
</a>
</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">
<EditableField
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 &&
<Input
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"
/>
}
</EditableField>
<EditableField
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}
/>
</EditableField>
<EditableField
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}
/>
}
</EditableField>
<EditableField
name="demographics_user_ethnicity"
type="select"
hidden
value={this.ethnicityFieldDisplay()}
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}
/>
</EditableField>
<EditableField
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}
/>
<EditableField
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}
/>
<EditableField
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}
/>
<EditableField
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}
/>
<EditableField
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 &&
<Input
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"
<EditableField
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}
/>
}
</EditableField>
<EditableField
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}
/>
<EditableField
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}
/>
<EditableField
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}
/>
<EditableField
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}
/>
<EditableField
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 &&
<Input
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"
/>
}
</EditableField>
<EditableField
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}
/>
<EditableField
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,
@@ -254,10 +319,13 @@ DemographicsSection.propTypes = {
demographics_current_work_sector: PropTypes.string,
demographics_future_work_sector: PropTypes.string,
}).isRequired,
formErrors: PropTypes.shape({
demographicsError: PropTypes.string,
}).isRequired,
updateDraft: PropTypes.func.isRequired
};
export default connect(demographicsSectionSelector, {
saveMultipleSettings,
updateDraft,
})(injectIntl(DemographicsSection))
})(injectIntl(DemographicsSection));

View File

@@ -1,11 +1,13 @@
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',
@@ -21,26 +23,6 @@ const messages = defineMessages({
defaultMessage: 'Select a gender identity',
description: 'Placeholder for the gender identity options dropdown.',
},
'account.settings.field.demographics.gender.options.woman': {
id: 'account.settings.field.demographics.gender.options.woman',
defaultMessage: 'Woman',
description: 'The label for the woman gender identity option.',
},
'account.settings.field.demographics.gender.options.man': {
id: 'account.settings.field.demographics.gender.options.man',
defaultMessage: 'Man',
description: 'The label for the man gender identity option.',
},
'account.settings.field.demographics.gender.options.non-binary': {
id: 'account.settings.field.demographics.gender.options.non-binary',
defaultMessage: 'Non-binary',
description: 'The label for the non-binary gender identity option.',
},
'account.settings.field.demographics.gender.options.self-describe': {
id: 'account.settings.field.demographics.gender.options.self-describe',
defaultMessage: 'Prefer to self-describe',
description: 'The label for self-describe gender identity option.',
},
'account.settings.field.demographics.gender_description': {
id: 'account.settings.field.demographics.gender_description',
defaultMessage: 'Gender identity description',
@@ -51,6 +33,7 @@ const messages = defineMessages({
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',
@@ -66,46 +49,7 @@ const messages = defineMessages({
defaultMessage: 'Select all that apply', // TODO: Is this the desired text?
description: 'Placeholder for the ethnic background options field.',
},
'account.settings.field.demographics.ethnicity.options.american-indian-or-alaska-native': {
id: 'account.settings.field.demographics.ethnicity.options.american-indian-or-alaska-native',
defaultMessage: 'American Indian or Alaska Native',
description: 'The label for the American Indian or Alaska Native ethnicity option.',
},
'account.settings.field.demographics.ethnicity.options.asian': {
id: 'account.settings.field.demographics.ethnicity.options.asian',
defaultMessage: 'Asian',
description: 'The label for the Asian ethnicity option.',
},
'account.settings.field.demographics.ethnicity.options.black-or-african-american': {
id: 'account.settings.field.demographics.ethnicity.options.black-or-african-american',
defaultMessage: 'Black or African American',
description: 'The label for the Black or African American ethnicity option.',
},
'account.settings.field.demographics.ethnicity.options.hispanic-latin-spanish': {
id: 'account.settings.field.demographics.ethnicity.options.hispanic-latin-spanish',
defaultMessage: 'Hispanic, Latin, or Spanish origin',
description: 'The label for the Hispanic, Latin, or Spanish origin ethnicity option.',
},
'account.settings.field.demographics.ethnicity.options.middle-eastern-or-north-african': {
id: 'account.settings.field.demographics.ethnicity.options.middle-eastern-or-north-african',
defaultMessage: 'Middle Eastern or North African',
description: 'The label for the Middle Eastern or North African ethnicity option.',
},
'account.settings.field.demographics.ethnicity.options.native-hawaiian-or-pacific-islander': {
id: 'account.settings.field.demographics.ethnicity.options.native-hawaiian-or-pacific-islander',
defaultMessage: 'Native Hawaiian or Other Pacific Islander',
description: 'The label for the Native Hawaiian or Other Pacific Islander ethnicity option.',
},
'account.settings.field.demographics.ethnicity.options.white': {
id: 'account.settings.field.demographics.ethnicity.options.white',
defaultMessage: 'White',
description: 'The label for the White ethnicity option.',
},
'account.settings.field.demographics.ethnicity.options.other': {
id: 'account.settings.field.demographics.ethnicity.options.other',
defaultMessage: 'Some other race, ethnicity, or origin',
description: 'The label for the Some other race, ethnicity, or origin ethnicity option.',
},
/* Income */
'account.settings.field.demographics.income': {
id: 'account.settings.field.demographics.income',
defaultMessage: 'Family income',
@@ -121,36 +65,7 @@ const messages = defineMessages({
defaultMessage: 'Select a family income range',
description: 'Placeholder for the household income dropdown.',
},
'account.settings.field.demographics.income.options.less-than-10k': {
id: 'account.settings.field.demographics.income.options.less-than-10k',
defaultMessage: 'Less than US $10,000',
description: 'The label for the less than US $10,000 income option.',
},
'account.settings.field.demographics.income.options.10k-25k': {
id: 'account.settings.field.demographics.income.options.10k-25k',
defaultMessage: 'US $10,000 - $25,000',
description: 'The label for the US $10,000 - $25,000 income option.',
},
'account.settings.field.demographics.income.options.25k-50k': {
id: 'account.settings.field.demographics.income.options.25k-50k',
defaultMessage: 'US $25,000 - $50,000',
description: 'The label for the US $25,000 - $50,000 income option.',
},
'account.settings.field.demographics.income.options.50k-75k': {
id: 'account.settings.field.demographics.income.options.50k-75k',
defaultMessage: 'US $50,000 - $75,000',
description: 'The label for the US $50,000 - $75,000 income option.',
},
'account.settings.field.demographics.income.options.over-100k': {
id: 'account.settings.field.demographics.income.options.over-100k',
defaultMessage: 'Over US $100,000',
description: 'The label for the over US $100,000 income option.',
},
'account.settings.field.demographics.income.options.unsure': {
id: 'account.settings.field.demographics.income.options.unsure',
defaultMessage: 'I don\'t know',
description: 'The label for the I don\'t know income option.',
},
/* Military history */
'account.settings.field.demographics.military_history': {
id: 'account.settings.field.demographics.military_history',
defaultMessage: 'U.S. Military status',
@@ -166,26 +81,7 @@ const messages = defineMessages({
defaultMessage: 'Select military status',
description: 'Placeholder for the military history dropdown.',
},
'account.settings.field.demographics.military_history.options.never-served': {
id: 'account.settings.field.demographics.income.options.never-served',
defaultMessage: 'Never served in the military',
description: 'The label for the never served in the military military history option.',
},
'account.settings.field.demographics.military_history.options.training': {
id: 'account.settings.field.demographics.income.options.training',
defaultMessage: 'Only on active duty for training',
description: 'The label for the only on active duty for training military history option.',
},
'account.settings.field.demographics.military_history.options.active': {
id: 'account.settings.field.demographics.income.options.active',
defaultMessage: 'Now on active duty',
description: 'The label for the now on active duty military history option.',
},
'account.settings.field.demographics.military_history.options.previously-active': {
id: 'account.settings.field.demographics.income.options.previously-active',
defaultMessage: 'On active duty in the past, but not now',
description: 'The label for the on active duty in the past, but not now military history option.',
},
/* Learner and family education level */
'account.settings.field.demographics.learner_education_level': {
id: 'account.settings.field.demographics.learner_education_level',
defaultMessage: 'Your education level',
@@ -211,56 +107,7 @@ const messages = defineMessages({
defaultMessage: 'Select education level',
description: 'Placeholder for the education level options dropdown.',
},
'account.settings.field.demographics.education_level.options.no-high-school': {
id: 'account.settings.field.demographics.education_level.options.no-high-school',
defaultMessage: 'No High School',
description: 'The label for the no high school education level option.',
},
'account.settings.field.demographics.education_level.options.some-high-school': {
id: 'account.settings.field.demographics.education_level.options.some-high-school',
defaultMessage: 'Some High School',
description: 'The label for the some high school education level option.',
},
'account.settings.field.demographics.education_level.options.high-school-ged-equivalent': {
id: 'account.settings.field.demographics.education_level.options.high-school-ged-equivalent',
defaultMessage: 'High School diploma, GED, or equivalent',
description: 'The label for the high school diploma, GED, or equivalent education level option.',
},
'account.settings.field.demographics.education_level.options.some-college': {
id: 'account.settings.field.demographics.education_level.options.some-college',
defaultMessage: 'Some college, but no degree',
description: 'The label for the some college, but no degree education level option.',
},
'account.settings.field.demographics.education_level.options.some-college': {
id: 'account.settings.field.demographics.education_level.options.some-college',
defaultMessage: 'Some college, but no degree',
description: 'The label for the some college, but no degree education level option.',
},
'account.settings.field.demographics.education_level.options.associates': {
id: 'account.settings.field.demographics.education_level.options.associates',
defaultMessage: 'Associates degree',
description: 'The label for the Associates degree education level option.',
},
'account.settings.field.demographics.education_level.options.bachelors': {
id: 'account.settings.field.demographics.education_level.options.bachelors',
defaultMessage: 'Bachelors degree',
description: 'The label for the Bachelors degree education level option.',
},
'account.settings.field.demographics.education_level.options.masters': {
id: 'account.settings.field.demographics.education_level.options.masters',
defaultMessage: 'Masters degree',
description: 'The label for the Masters degree education level option.',
},
'account.settings.field.demographics.education_level.options.professional': {
id: 'account.settings.field.demographics.education_level.options.professional',
defaultMessage: 'Professional degree',
description: 'The label for the Professional degree education level option.',
},
'account.settings.field.demographics.education_level.options.doctorate': {
id: 'account.settings.field.demographics.education_level.options.doctorate',
defaultMessage: 'Doctorate degree',
description: 'The label for the Doctorate degree education level option.',
},
/* Work status */
'account.settings.field.demographics.work_status': {
id: 'account.settings.field.demographics.work_status',
defaultMessage: 'Employment status',
@@ -276,41 +123,6 @@ const messages = defineMessages({
defaultMessage: 'Select employment status',
description: 'Placeholder for the work status options dropdown.',
},
'account.settings.field.demographics.work_status.options.full-time': {
id: 'account.settings.field.demographics.work_status.options.full-time',
defaultMessage: 'Employed, working full-time',
description: 'The label for the employed, working full-time work status option.',
},
'account.settings.field.demographics.work_status.options.part-time': {
id: 'account.settings.field.demographics.work_status.options.part-time',
defaultMessage: 'Employed, working part-time',
description: 'The label for the employed, working part-time work status option.',
},
'account.settings.field.demographics.work_status.options.not-employed-looking': {
id: 'account.settings.field.demographics.work_status.options.not-employed-looking',
defaultMessage: 'Not employed, looking for work',
description: 'The label for the not employed, looking for work work status option.',
},
'account.settings.field.demographics.work_status.options.not-employed-not-looking': {
id: 'account.settings.field.demographics.work_status.options.not-employed-not-looking',
defaultMessage: 'Not employed, not looking for work',
description: 'The label for the not employed, not looking for work work status option.',
},
'account.settings.field.demographics.work_status.options.unable': {
id: 'account.settings.field.demographics.work_status.options.unable',
defaultMessage: 'Unable to work',
description: 'The label for the unable to work work status option.',
},
'account.settings.field.demographics.work_status.options.retired': {
id: 'account.settings.field.demographics.work_status.options.retired',
defaultMessage: 'Retired',
description: 'The label for the retired work status option.',
},
'account.settings.field.demographics.work_status.options.other': {
id: 'account.settings.field.demographics.work_status.options.other',
defaultMessage: 'Other',
description: 'The label for the other work status option.',
},
'account.settings.field.demographics.work_status_description': {
id: 'account.settings.field.demographics.work_status_description',
defaultMessage: 'Employment status description',
@@ -321,6 +133,7 @@ const messages = defineMessages({
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',
@@ -346,110 +159,11 @@ const messages = defineMessages({
defaultMessage: 'Select work industry',
description: 'Placeholder for the work sector options dropdown.',
},
'account.settings.field.demographics.work_sector.options.accommodation-food': {
id: 'account.settings.field.demographics.work_sector.options.accommodation-food',
defaultMessage: 'Accommodation and Food Services',
description: 'The label for the Accommodation and Food Services work sector option.',
},
'account.settings.field.demographics.work_sector.options.administrative-support-waste-remediation': {
id: 'account.settings.field.demographics.work_sector.options.administrative-support-waste-remediation',
defaultMessage: 'Administrative and Support and Waste Management and Remediation Services',
description: 'The label for the Administrative and Support and Waste Management and Remediation Services work sector option.',
},
'account.settings.field.demographics.work_sector.options.agriculture-forestry-fishing-hunting': {
id: 'account.settings.field.demographics.work_sector.options.agriculture-forestry-fishing-hunting',
defaultMessage: 'Agriculture, Forestry, Fishing and Hunting',
description: 'The label for the Agriculture, Forestry, Fishing and Hunting work sector option.',
},
'account.settings.field.demographics.work_sector.options.arts-entertainment-recreation': {
id: 'account.settings.field.demographics.work_sector.options.arts-entertainment-recreation',
defaultMessage: 'Arts, Entertainment, and Recreation',
description: 'The label for the Arts, Entertainment, and Recreation work sector option.',
},
'account.settings.field.demographics.work_sector.options.construction': {
id: 'account.settings.field.demographics.work_sector.options.construction',
defaultMessage: 'Construction',
description: 'The label for the Construction work sector option.',
},
'account.settings.field.demographics.work_sector.options.educational': {
id: 'account.settings.field.demographics.work_sector.options.educational',
defaultMessage: 'Education Services',
description: 'The label for the Education Services work sector option.',
},
'account.settings.field.demographics.work_sector.options.finance-insurance': {
id: 'account.settings.field.demographics.work_sector.options.finance-insurance',
defaultMessage: 'Finance and Insurance',
description: 'The label for the Finance and Insurance work sector option.',
},
'account.settings.field.demographics.work_sector.options.healthcare-social': {
id: 'account.settings.field.demographics.work_sector.options.healthcare-social',
defaultMessage: 'Health Care and Social Assistance',
description: 'The label for the Health Care and Social Assistance work sector option.',
},
'account.settings.field.demographics.work_sector.options.information': {
id: 'account.settings.field.demographics.work_sector.options.information',
defaultMessage: 'Information',
description: 'The label for the Information work sector option.',
},
'account.settings.field.demographics.work_sector.options.management': {
id: 'account.settings.field.demographics.work_sector.options.management',
defaultMessage: 'Management of Companies and Enterprises',
description: 'The label for the Management of Companies and Enterprises work sector option.',
},
'account.settings.field.demographics.work_sector.options.manufacturing': {
id: 'account.settings.field.demographics.work_sector.options.manufacturing',
defaultMessage: 'Manufacturing',
description: 'The label for the Manufacturing work sector option.',
},
'account.settings.field.demographics.work_sector.options.mining-quarry-oil-gas': {
id: 'account.settings.field.demographics.work_sector.options.mining-quarry-oil-gas',
defaultMessage: 'Mining, Quarrying, and Oil and Gas Extraction',
description: 'The label for the Mining, Quarrying, and Oil and Gas Extraction work sector option.',
},
'account.settings.field.demographics.work_sector.options.professional-scientific-technical': {
id: 'account.settings.field.demographics.work_sector.options.professional-scientific-technical',
defaultMessage: 'Professional, Scientific, and Technical Services',
description: 'The label for the Professional, Scientific, and Technical Services work sector option.',
},
'account.settings.field.demographics.work_sector.options.public-admin': {
id: 'account.settings.field.demographics.work_sector.options.public-admin',
defaultMessage: 'Public Administration',
description: 'The label for the Public Administration work sector option.',
},
'account.settings.field.demographics.work_sector.options.real-estate': {
id: 'account.settings.field.demographics.work_sector.options.real-estate',
defaultMessage: 'Real Estate and Rental and Leasing',
description: 'The label for the Real Estate and Rental and Leasing work sector option.',
},
'account.settings.field.demographics.work_sector.options.retail': {
id: 'account.settings.field.demographics.work_sector.options.retail',
defaultMessage: 'Retail Trade',
description: 'The label for the Retail Trade work sector option.',
},
'account.settings.field.demographics.work_sector.options.transport-warehousing': {
id: 'account.settings.field.demographics.work_sector.options.transport-warehousing',
defaultMessage: 'Transportation and Warehousing',
description: 'The label for the Transportation and Warehousing work sector option.',
},
'account.settings.field.demographics.work_sector.options.utilities': {
id: 'account.settings.field.demographics.work_sector.options.utilities',
defaultMessage: 'Utilities',
description: 'The label for the Utilities work sector option.',
},
'account.settings.field.demographics.work_sector.options.trade': {
id: 'account.settings.field.demographics.work_sector.options.trade',
defaultMessage: 'Wholesale Trade',
description: 'The label for the Wholesale Trade work sector option.',
},
'account.settings.field.demographics.work_sector.options.other': {
id: 'account.settings.field.demographics.work_sector.options.other',
defaultMessage: 'Other',
description: 'The label for the Other work sector option.',
},
'account.settings.field.demographics.options.declined': {
id: 'account.settings.field.demographics.options.declined',
defaultMessage: 'Prefer not to respond',
description: 'The label for the declined option.',
/* Legal copy link text */
'account.settings.section.demographics.why': {
id: 'account.settings.section.demographics.why',
defaultMessage: 'Why does edX collect this information?',
description: 'Link text for a link to external legal text',
},
});

View File

@@ -1,21 +1,58 @@
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)
.post(requestUrl, commitValues, requestConfig)
.catch((error) => {
const apiError = Object.create(error);
const apiError = createDemographicsError(error);
throw apiError;
}));
@@ -36,7 +73,29 @@ export async function getDemographics(userId) {
data = convertData(data, FROM);
} catch (error) {
data = await postDemographics(userId);
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;
@@ -55,9 +114,28 @@ export async function patchDemographics(userId, commitValues) {
({ data } = await getAuthenticatedHttpClient()
.patch(requestUrl, convertedCommitValues)
.catch((error) => {
const apiError = Object.create(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,5 +1,18 @@
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"]

View File

@@ -0,0 +1,583 @@
import * as auth from '@edx/frontend-platform/auth';
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
import DemographicsSection from '../DemographicsSection';
import { Provider } from 'react-redux';
import React from 'react';
import configureStore from 'redux-mock-store';
import renderer from 'react-test-renderer';
jest.mock('@edx/frontend-platform/auth');
const IntlDemographicsSection = injectIntl(DemographicsSection);
jest.mock('../../data/selectors', () => {
return 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: {},
};
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: ""
}
}
const wrapper = renderer.create(reduxWrapper(<IntlDemographicsSection {...props} />)).toJSON();
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,3688 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`DemographicsSection should render 1`] = `
<div
className="account-section"
id="demographics-information"
>
<h2
className="section-heading"
>
Optional Information
</h2>
<p>
<a
href="http://localhost:5335/demographics"
target="_blank"
>
Why does edX collect this information?
</a>
</p>
<div
id="demographics-fields"
>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
>
<div
className="form-group"
>
<div
className="d-flex align-items-start"
>
<h6
aria-level="3"
>
Gender identity
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
data-icon="pencil-alt"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
fill="currentColor"
style={Object {}}
/>
</svg>
Edit
</button>
</div>
<p
data-hj-suppress={true}
>
Prefer not to respond
</p>
<p
className="small text-muted mt-n2"
/>
</div>
</div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
>
<div
className="form-group"
>
<div
className="d-flex align-items-start"
>
<h6
aria-level="3"
>
Race/Ethnicity identity
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
data-icon="pencil-alt"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
fill="currentColor"
style={Object {}}
/>
</svg>
Edit
</button>
</div>
<p
data-hj-suppress={true}
>
<button
className="btn btn-link p-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Add race/ethnicity identity
</button>
</p>
<p
className="small text-muted mt-n2"
/>
</div>
</div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
>
<div
className="form-group"
>
<div
className="d-flex align-items-start"
>
<h6
aria-level="3"
>
Family income
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
data-icon="pencil-alt"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
fill="currentColor"
style={Object {}}
/>
</svg>
Edit
</button>
</div>
<p
data-hj-suppress={true}
>
Prefer not to respond
</p>
<p
className="small text-muted mt-n2"
/>
</div>
</div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
>
<div
className="form-group"
>
<div
className="d-flex align-items-start"
>
<h6
aria-level="3"
>
U.S. Military status
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
data-icon="pencil-alt"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
fill="currentColor"
style={Object {}}
/>
</svg>
Edit
</button>
</div>
<p
data-hj-suppress={true}
>
Prefer not to respond
</p>
<p
className="small text-muted mt-n2"
/>
</div>
</div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
>
<div
className="form-group"
>
<div
className="d-flex align-items-start"
>
<h6
aria-level="3"
>
Your education level
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
data-icon="pencil-alt"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
fill="currentColor"
style={Object {}}
/>
</svg>
Edit
</button>
</div>
<p
data-hj-suppress={true}
>
Prefer not to respond
</p>
<p
className="small text-muted mt-n2"
/>
</div>
</div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
>
<div
className="form-group"
>
<div
className="d-flex align-items-start"
>
<h6
aria-level="3"
>
Parents/Guardians education level
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
data-icon="pencil-alt"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
fill="currentColor"
style={Object {}}
/>
</svg>
Edit
</button>
</div>
<p
data-hj-suppress={true}
>
Prefer not to respond
</p>
<p
className="small text-muted mt-n2"
/>
</div>
</div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
>
<div
className="form-group"
>
<div
className="d-flex align-items-start"
>
<h6
aria-level="3"
>
Employment status
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
data-icon="pencil-alt"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
fill="currentColor"
style={Object {}}
/>
</svg>
Edit
</button>
</div>
<p
data-hj-suppress={true}
>
Prefer not to respond
</p>
<p
className="small text-muted mt-n2"
/>
</div>
</div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
>
<div
className="form-group"
>
<div
className="d-flex align-items-start"
>
<h6
aria-level="3"
>
Current work industry
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
data-icon="pencil-alt"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
fill="currentColor"
style={Object {}}
/>
</svg>
Edit
</button>
</div>
<p
data-hj-suppress={true}
>
Prefer not to respond
</p>
<p
className="small text-muted mt-n2"
/>
</div>
</div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
>
<div
className="form-group"
>
<div
className="d-flex align-items-start"
>
<h6
aria-level="3"
>
Future work industry
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
data-icon="pencil-alt"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
fill="currentColor"
style={Object {}}
/>
</svg>
Edit
</button>
</div>
<p
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"
id="demographics-information"
>
<h2
className="section-heading"
>
Optional Information
</h2>
<p>
<a
href="http://localhost:5335/demographics"
target="_blank"
>
Why does edX collect this information?
</a>
</p>
<div
tabIndex="-1"
>
<div
className="alert d-flex align-items-start alert alert-danger"
>
<div />
<div>
<span>
An error occurred attempting to retrieve or save your account information. Please try again later.
</span>
</div>
</div>
</div>
<div
id="demographics-fields"
>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
>
<div
className="form-group"
>
<div
className="d-flex align-items-start"
>
<h6
aria-level="3"
>
Gender identity
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
data-icon="pencil-alt"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
fill="currentColor"
style={Object {}}
/>
</svg>
Edit
</button>
</div>
<p
data-hj-suppress={true}
>
Prefer not to respond
</p>
<p
className="small text-muted mt-n2"
/>
</div>
</div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
>
<div
className="form-group"
>
<div
className="d-flex align-items-start"
>
<h6
aria-level="3"
>
Race/Ethnicity identity
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
data-icon="pencil-alt"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
fill="currentColor"
style={Object {}}
/>
</svg>
Edit
</button>
</div>
<p
data-hj-suppress={true}
>
<button
className="btn btn-link p-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Add race/ethnicity identity
</button>
</p>
<p
className="small text-muted mt-n2"
/>
</div>
</div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
>
<div
className="form-group"
>
<div
className="d-flex align-items-start"
>
<h6
aria-level="3"
>
Family income
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
data-icon="pencil-alt"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
fill="currentColor"
style={Object {}}
/>
</svg>
Edit
</button>
</div>
<p
data-hj-suppress={true}
>
Prefer not to respond
</p>
<p
className="small text-muted mt-n2"
/>
</div>
</div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
>
<div
className="form-group"
>
<div
className="d-flex align-items-start"
>
<h6
aria-level="3"
>
U.S. Military status
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
data-icon="pencil-alt"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
fill="currentColor"
style={Object {}}
/>
</svg>
Edit
</button>
</div>
<p
data-hj-suppress={true}
>
Prefer not to respond
</p>
<p
className="small text-muted mt-n2"
/>
</div>
</div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
>
<div
className="form-group"
>
<div
className="d-flex align-items-start"
>
<h6
aria-level="3"
>
Your education level
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
data-icon="pencil-alt"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
fill="currentColor"
style={Object {}}
/>
</svg>
Edit
</button>
</div>
<p
data-hj-suppress={true}
>
Prefer not to respond
</p>
<p
className="small text-muted mt-n2"
/>
</div>
</div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
>
<div
className="form-group"
>
<div
className="d-flex align-items-start"
>
<h6
aria-level="3"
>
Parents/Guardians education level
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
data-icon="pencil-alt"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
fill="currentColor"
style={Object {}}
/>
</svg>
Edit
</button>
</div>
<p
data-hj-suppress={true}
>
Prefer not to respond
</p>
<p
className="small text-muted mt-n2"
/>
</div>
</div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
>
<div
className="form-group"
>
<div
className="d-flex align-items-start"
>
<h6
aria-level="3"
>
Employment status
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
data-icon="pencil-alt"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
fill="currentColor"
style={Object {}}
/>
</svg>
Edit
</button>
</div>
<p
data-hj-suppress={true}
>
Prefer not to respond
</p>
<p
className="small text-muted mt-n2"
/>
</div>
</div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
>
<div
className="form-group"
>
<div
className="d-flex align-items-start"
>
<h6
aria-level="3"
>
Current work industry
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
data-icon="pencil-alt"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
fill="currentColor"
style={Object {}}
/>
</svg>
Edit
</button>
</div>
<p
data-hj-suppress={true}
>
Prefer not to respond
</p>
<p
className="small text-muted mt-n2"
/>
</div>
</div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
>
<div
className="form-group"
>
<div
className="d-flex align-items-start"
>
<h6
aria-level="3"
>
Future work industry
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
data-icon="pencil-alt"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
fill="currentColor"
style={Object {}}
/>
</svg>
Edit
</button>
</div>
<p
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"
id="demographics-information"
>
<h2
className="section-heading"
>
Optional Information
</h2>
<p>
<a
href="http://localhost:5335/demographics"
target="_blank"
>
Why does edX collect this information?
</a>
</p>
<div
tabIndex="-1"
>
<div
className="alert d-flex align-items-start alert alert-danger"
>
<div />
<div>
<span>
An error occurred attempting to retrieve or save your account information. Please try again later.
</span>
</div>
</div>
</div>
</div>
`;
exports[`DemographicsSection should render ethnicity correctly when multiple options are selected 1`] = `
<div
className="account-section"
id="demographics-information"
>
<h2
className="section-heading"
>
Optional Information
</h2>
<p>
<a
href="http://localhost:5335/demographics"
target="_blank"
>
Why does edX collect this information?
</a>
</p>
<div
id="demographics-fields"
>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
>
<div
className="form-group"
>
<div
className="d-flex align-items-start"
>
<h6
aria-level="3"
>
Gender identity
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
data-icon="pencil-alt"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
fill="currentColor"
style={Object {}}
/>
</svg>
Edit
</button>
</div>
<p
data-hj-suppress={true}
>
Prefer not to respond
</p>
<p
className="small text-muted mt-n2"
/>
</div>
</div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
>
<div
className="form-group"
>
<div
className="d-flex align-items-start"
>
<h6
aria-level="3"
>
Race/Ethnicity identity
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
data-icon="pencil-alt"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
fill="currentColor"
style={Object {}}
/>
</svg>
Edit
</button>
</div>
<p
data-hj-suppress={true}
>
Hispanic, Latin, or Spanish origin, White
</p>
<p
className="small text-muted mt-n2"
/>
</div>
</div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
>
<div
className="form-group"
>
<div
className="d-flex align-items-start"
>
<h6
aria-level="3"
>
Family income
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
data-icon="pencil-alt"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
fill="currentColor"
style={Object {}}
/>
</svg>
Edit
</button>
</div>
<p
data-hj-suppress={true}
>
Prefer not to respond
</p>
<p
className="small text-muted mt-n2"
/>
</div>
</div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
>
<div
className="form-group"
>
<div
className="d-flex align-items-start"
>
<h6
aria-level="3"
>
U.S. Military status
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
data-icon="pencil-alt"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
fill="currentColor"
style={Object {}}
/>
</svg>
Edit
</button>
</div>
<p
data-hj-suppress={true}
>
Prefer not to respond
</p>
<p
className="small text-muted mt-n2"
/>
</div>
</div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
>
<div
className="form-group"
>
<div
className="d-flex align-items-start"
>
<h6
aria-level="3"
>
Your education level
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
data-icon="pencil-alt"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
fill="currentColor"
style={Object {}}
/>
</svg>
Edit
</button>
</div>
<p
data-hj-suppress={true}
>
Prefer not to respond
</p>
<p
className="small text-muted mt-n2"
/>
</div>
</div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
>
<div
className="form-group"
>
<div
className="d-flex align-items-start"
>
<h6
aria-level="3"
>
Parents/Guardians education level
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
data-icon="pencil-alt"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
fill="currentColor"
style={Object {}}
/>
</svg>
Edit
</button>
</div>
<p
data-hj-suppress={true}
>
Prefer not to respond
</p>
<p
className="small text-muted mt-n2"
/>
</div>
</div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
>
<div
className="form-group"
>
<div
className="d-flex align-items-start"
>
<h6
aria-level="3"
>
Employment status
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
data-icon="pencil-alt"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
fill="currentColor"
style={Object {}}
/>
</svg>
Edit
</button>
</div>
<p
data-hj-suppress={true}
>
Prefer not to respond
</p>
<p
className="small text-muted mt-n2"
/>
</div>
</div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
>
<div
className="form-group"
>
<div
className="d-flex align-items-start"
>
<h6
aria-level="3"
>
Current work industry
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
data-icon="pencil-alt"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
fill="currentColor"
style={Object {}}
/>
</svg>
Edit
</button>
</div>
<p
data-hj-suppress={true}
>
Prefer not to respond
</p>
<p
className="small text-muted mt-n2"
/>
</div>
</div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
>
<div
className="form-group"
>
<div
className="d-flex align-items-start"
>
<h6
aria-level="3"
>
Future work industry
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
data-icon="pencil-alt"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
fill="currentColor"
style={Object {}}
/>
</svg>
Edit
</button>
</div>
<p
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"
id="demographics-information"
>
<h2
className="section-heading"
>
Optional Information
</h2>
<p>
<a
href="http://localhost:5335/demographics"
target="_blank"
>
Why does edX collect this information?
</a>
</p>
<div
id="demographics-fields"
>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
>
<div
className="form-group"
>
<div
className="d-flex align-items-start"
>
<h6
aria-level="3"
>
Gender identity
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
data-icon="pencil-alt"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
fill="currentColor"
style={Object {}}
/>
</svg>
Edit
</button>
</div>
<p
data-hj-suppress={true}
>
Prefer not to respond
</p>
<p
className="small text-muted mt-n2"
/>
</div>
</div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
>
<div
className="form-group"
>
<div
className="d-flex align-items-start"
>
<h6
aria-level="3"
>
Race/Ethnicity identity
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
data-icon="pencil-alt"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
fill="currentColor"
style={Object {}}
/>
</svg>
Edit
</button>
</div>
<p
data-hj-suppress={true}
>
Asian
</p>
<p
className="small text-muted mt-n2"
/>
</div>
</div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
>
<div
className="form-group"
>
<div
className="d-flex align-items-start"
>
<h6
aria-level="3"
>
Family income
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
data-icon="pencil-alt"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
fill="currentColor"
style={Object {}}
/>
</svg>
Edit
</button>
</div>
<p
data-hj-suppress={true}
>
Prefer not to respond
</p>
<p
className="small text-muted mt-n2"
/>
</div>
</div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
>
<div
className="form-group"
>
<div
className="d-flex align-items-start"
>
<h6
aria-level="3"
>
U.S. Military status
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
data-icon="pencil-alt"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
fill="currentColor"
style={Object {}}
/>
</svg>
Edit
</button>
</div>
<p
data-hj-suppress={true}
>
Prefer not to respond
</p>
<p
className="small text-muted mt-n2"
/>
</div>
</div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
>
<div
className="form-group"
>
<div
className="d-flex align-items-start"
>
<h6
aria-level="3"
>
Your education level
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
data-icon="pencil-alt"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
fill="currentColor"
style={Object {}}
/>
</svg>
Edit
</button>
</div>
<p
data-hj-suppress={true}
>
Prefer not to respond
</p>
<p
className="small text-muted mt-n2"
/>
</div>
</div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
>
<div
className="form-group"
>
<div
className="d-flex align-items-start"
>
<h6
aria-level="3"
>
Parents/Guardians education level
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
data-icon="pencil-alt"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
fill="currentColor"
style={Object {}}
/>
</svg>
Edit
</button>
</div>
<p
data-hj-suppress={true}
>
Prefer not to respond
</p>
<p
className="small text-muted mt-n2"
/>
</div>
</div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
>
<div
className="form-group"
>
<div
className="d-flex align-items-start"
>
<h6
aria-level="3"
>
Employment status
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
data-icon="pencil-alt"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
fill="currentColor"
style={Object {}}
/>
</svg>
Edit
</button>
</div>
<p
data-hj-suppress={true}
>
Prefer not to respond
</p>
<p
className="small text-muted mt-n2"
/>
</div>
</div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
>
<div
className="form-group"
>
<div
className="d-flex align-items-start"
>
<h6
aria-level="3"
>
Current work industry
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
data-icon="pencil-alt"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
fill="currentColor"
style={Object {}}
/>
</svg>
Edit
</button>
</div>
<p
data-hj-suppress={true}
>
Prefer not to respond
</p>
<p
className="small text-muted mt-n2"
/>
</div>
</div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
>
<div
className="form-group"
>
<div
className="d-flex align-items-start"
>
<h6
aria-level="3"
>
Future work industry
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
data-icon="pencil-alt"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
fill="currentColor"
style={Object {}}
/>
</svg>
Edit
</button>
</div>
<p
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"
id="demographics-information"
>
<h2
className="section-heading"
>
Optional Information
</h2>
<p>
<a
href="http://localhost:5335/demographics"
target="_blank"
>
Why does edX collect this information?
</a>
</p>
<div
id="demographics-fields"
>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
>
<div
className="form-group"
>
<div
className="d-flex align-items-start"
>
<h6
aria-level="3"
>
Gender identity
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
data-icon="pencil-alt"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
fill="currentColor"
style={Object {}}
/>
</svg>
Edit
</button>
</div>
<p
data-hj-suppress={true}
>
Prefer not to respond
</p>
<p
className="small text-muted mt-n2"
/>
</div>
</div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
>
<div
className="form-group"
>
<div
className="d-flex align-items-start"
>
<h6
aria-level="3"
>
Race/Ethnicity identity
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
data-icon="pencil-alt"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
fill="currentColor"
style={Object {}}
/>
</svg>
Edit
</button>
</div>
<p
data-hj-suppress={true}
>
<button
className="btn btn-link p-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Add race/ethnicity identity
</button>
</p>
<p
className="small text-muted mt-n2"
/>
</div>
</div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
>
<div
className="form-group"
>
<div
className="d-flex align-items-start"
>
<h6
aria-level="3"
>
Family income
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
data-icon="pencil-alt"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
fill="currentColor"
style={Object {}}
/>
</svg>
Edit
</button>
</div>
<p
data-hj-suppress={true}
>
Prefer not to respond
</p>
<p
className="small text-muted mt-n2"
/>
</div>
</div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
>
<div
className="form-group"
>
<div
className="d-flex align-items-start"
>
<h6
aria-level="3"
>
U.S. Military status
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
data-icon="pencil-alt"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
fill="currentColor"
style={Object {}}
/>
</svg>
Edit
</button>
</div>
<p
data-hj-suppress={true}
>
Prefer not to respond
</p>
<p
className="small text-muted mt-n2"
/>
</div>
</div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
>
<div
className="form-group"
>
<div
className="d-flex align-items-start"
>
<h6
aria-level="3"
>
Your education level
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
data-icon="pencil-alt"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
fill="currentColor"
style={Object {}}
/>
</svg>
Edit
</button>
</div>
<p
data-hj-suppress={true}
>
Prefer not to respond
</p>
<p
className="small text-muted mt-n2"
/>
</div>
</div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
>
<div
className="form-group"
>
<div
className="d-flex align-items-start"
>
<h6
aria-level="3"
>
Parents/Guardians education level
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
data-icon="pencil-alt"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
fill="currentColor"
style={Object {}}
/>
</svg>
Edit
</button>
</div>
<p
data-hj-suppress={true}
>
Prefer not to respond
</p>
<p
className="small text-muted mt-n2"
/>
</div>
</div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
>
<div
className="form-group"
>
<div
className="d-flex align-items-start"
>
<h6
aria-level="3"
>
Employment status
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
data-icon="pencil-alt"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
fill="currentColor"
style={Object {}}
/>
</svg>
Edit
</button>
</div>
<p
data-hj-suppress={true}
>
Other: test
</p>
<p
className="small text-muted mt-n2"
/>
</div>
</div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
>
<div
className="form-group"
>
<div
className="d-flex align-items-start"
>
<h6
aria-level="3"
>
Current work industry
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
data-icon="pencil-alt"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
fill="currentColor"
style={Object {}}
/>
</svg>
Edit
</button>
</div>
<p
data-hj-suppress={true}
>
Prefer not to respond
</p>
<p
className="small text-muted mt-n2"
/>
</div>
</div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
>
<div
className="form-group"
>
<div
className="d-flex align-items-start"
>
<h6
aria-level="3"
>
Future work industry
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
data-icon="pencil-alt"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
fill="currentColor"
style={Object {}}
/>
</svg>
Edit
</button>
</div>
<p
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"
id="demographics-information"
>
<h2
className="section-heading"
>
Optional Information
</h2>
<p>
<a
href="http://localhost:5335/demographics"
target="_blank"
>
Why does edX collect this information?
</a>
</p>
<div
id="demographics-fields"
>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
>
<div
className="form-group"
>
<div
className="d-flex align-items-start"
>
<h6
aria-level="3"
>
Gender identity
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
data-icon="pencil-alt"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
fill="currentColor"
style={Object {}}
/>
</svg>
Edit
</button>
</div>
<p
data-hj-suppress={true}
>
Prefer to self describe: test
</p>
<p
className="small text-muted mt-n2"
/>
</div>
</div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
>
<div
className="form-group"
>
<div
className="d-flex align-items-start"
>
<h6
aria-level="3"
>
Race/Ethnicity identity
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
data-icon="pencil-alt"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
fill="currentColor"
style={Object {}}
/>
</svg>
Edit
</button>
</div>
<p
data-hj-suppress={true}
>
<button
className="btn btn-link p-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Add race/ethnicity identity
</button>
</p>
<p
className="small text-muted mt-n2"
/>
</div>
</div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
>
<div
className="form-group"
>
<div
className="d-flex align-items-start"
>
<h6
aria-level="3"
>
Family income
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
data-icon="pencil-alt"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
fill="currentColor"
style={Object {}}
/>
</svg>
Edit
</button>
</div>
<p
data-hj-suppress={true}
>
Prefer not to respond
</p>
<p
className="small text-muted mt-n2"
/>
</div>
</div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
>
<div
className="form-group"
>
<div
className="d-flex align-items-start"
>
<h6
aria-level="3"
>
U.S. Military status
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
data-icon="pencil-alt"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
fill="currentColor"
style={Object {}}
/>
</svg>
Edit
</button>
</div>
<p
data-hj-suppress={true}
>
Prefer not to respond
</p>
<p
className="small text-muted mt-n2"
/>
</div>
</div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
>
<div
className="form-group"
>
<div
className="d-flex align-items-start"
>
<h6
aria-level="3"
>
Your education level
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
data-icon="pencil-alt"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
fill="currentColor"
style={Object {}}
/>
</svg>
Edit
</button>
</div>
<p
data-hj-suppress={true}
>
Prefer not to respond
</p>
<p
className="small text-muted mt-n2"
/>
</div>
</div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
>
<div
className="form-group"
>
<div
className="d-flex align-items-start"
>
<h6
aria-level="3"
>
Parents/Guardians education level
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
data-icon="pencil-alt"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
fill="currentColor"
style={Object {}}
/>
</svg>
Edit
</button>
</div>
<p
data-hj-suppress={true}
>
Prefer not to respond
</p>
<p
className="small text-muted mt-n2"
/>
</div>
</div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
>
<div
className="form-group"
>
<div
className="d-flex align-items-start"
>
<h6
aria-level="3"
>
Employment status
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
data-icon="pencil-alt"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
fill="currentColor"
style={Object {}}
/>
</svg>
Edit
</button>
</div>
<p
data-hj-suppress={true}
>
Prefer not to respond
</p>
<p
className="small text-muted mt-n2"
/>
</div>
</div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
>
<div
className="form-group"
>
<div
className="d-flex align-items-start"
>
<h6
aria-level="3"
>
Current work industry
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
data-icon="pencil-alt"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
fill="currentColor"
style={Object {}}
/>
</svg>
Edit
</button>
</div>
<p
data-hj-suppress={true}
>
Prefer not to respond
</p>
<p
className="small text-muted mt-n2"
/>
</div>
</div>
</div>
<div
className="pgn-transition-replace-group position-relative"
style={
Object {
"height": null,
}
}
>
<div
style={
Object {
"padding": ".1px 0",
}
}
>
<div
className="form-group"
>
<div
className="d-flex align-items-start"
>
<h6
aria-level="3"
>
Future work industry
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
data-icon="pencil-alt"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
fill="currentColor"
style={Object {}}
/>
</svg>
Edit
</button>
</div>
<p
data-hj-suppress={true}
>
Prefer not to respond
</p>
<p
className="small text-muted mt-n2"
/>
</div>
</div>
</div>
</div>
</div>
`;

View File

@@ -0,0 +1,62 @@
import React from 'react';
import renderer from 'react-test-renderer';
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
import JumpNav from "../JumpNav";
import { BrowserRouter as Router } from 'react-router-dom';
import { mergeConfig, setConfig } from '@edx/frontend-platform';
const IntlJumpNav = injectIntl(JumpNav);
describe('JumpNav', () => {
mergeConfig({
ENABLE_DEMOGRAPHICS_COLLECTION: false,
});
let props = {};
beforeEach(() => {
props = {
intl: {},
displayDemographicsLink: false,
};
});
it('should not render Optional Information link', () => {
const tree = renderer.create((
// Had to wrap the following in a router or I will receive an error stating:
// "Invariant failed: You should not use <NavLink> outside a <Router>"
<Router>
<IntlProvider locale="en">
<IntlJumpNav {...props} />
</IntlProvider>
</Router>
))
.toJSON();
expect(tree).toMatchSnapshot();
});
it('should render Optional Information link', () => {
setConfig({
ENABLE_DEMOGRAPHICS_COLLECTION: true,
});
props = {
...props,
displayDemographicsLink: true,
}
const tree = renderer.create((
// Same as previous test
<Router>
<IntlProvider locale="en">
<IntlJumpNav {...props} />
</IntlProvider>
</Router>
))
.toJSON();
expect(tree).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,194 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`JumpNav should not render Optional Information link 1`] = `
<div
className="jump-nav"
>
<ul
className="list-unstyled"
style={Object {}}
>
<li
className=""
>
<a
aria-current="page"
className="active"
href="/#basic-information"
onClick={[Function]}
style={Object {}}
>
Account Information
</a>
</li>
<li
className=""
>
<a
aria-current="page"
className="active"
href="/#profile-information"
onClick={[Function]}
style={Object {}}
>
Profile Information
</a>
</li>
<li
className=""
>
<a
aria-current="page"
className="active"
href="/#social-media"
onClick={[Function]}
style={Object {}}
>
Social Media Links
</a>
</li>
<li
className=""
>
<a
aria-current="page"
className="active"
href="/#site-preferences"
onClick={[Function]}
style={Object {}}
>
Site Preferences
</a>
</li>
<li
className=""
>
<a
aria-current="page"
className="active"
href="/#linked-accounts"
onClick={[Function]}
style={Object {}}
>
Linked Accounts
</a>
</li>
<li
className=""
>
<a
aria-current="page"
className="active"
href="/#delete-account"
onClick={[Function]}
style={Object {}}
>
Delete My Account
</a>
</li>
</ul>
</div>
`;
exports[`JumpNav should render Optional Information link 1`] = `
<div
className="jump-nav"
>
<ul
className="list-unstyled"
style={Object {}}
>
<li
className=""
>
<a
aria-current="page"
className="active"
href="/#basic-information"
onClick={[Function]}
style={Object {}}
>
Account Information
</a>
</li>
<li
className=""
>
<a
aria-current="page"
className="active"
href="/#profile-information"
onClick={[Function]}
style={Object {}}
>
Profile Information
</a>
</li>
<li
className=""
>
<a
aria-current="page"
className="active"
href="/#demographics-information"
onClick={[Function]}
style={Object {}}
>
Optional Information
</a>
</li>
<li
className=""
>
<a
aria-current="page"
className="active"
href="/#social-media"
onClick={[Function]}
style={Object {}}
>
Social Media Links
</a>
</li>
<li
className=""
>
<a
aria-current="page"
className="active"
href="/#site-preferences"
onClick={[Function]}
style={Object {}}
>
Site Preferences
</a>
</li>
<li
className=""
>
<a
aria-current="page"
className="active"
href="/#linked-accounts"
onClick={[Function]}
style={Object {}}
>
Linked Accounts
</a>
</li>
<li
className=""
>
<a
aria-current="page"
className="active"
href="/#delete-account"
onClick={[Function]}
style={Object {}}
>
Delete My Account
</a>
</li>
</ul>
</div>
`;

View File

@@ -123,66 +123,29 @@
"account.settings.delete.account.modal.after.text": "Account deletion, including removal from email lists, may take a few weeks to fully process through our system. If you want to opt-out of emails before then, please unsubscribe from the footer of any email.",
"account.settings.delete.account.modal.after.button": "Close",
"account.settings.delete.account.text.3": "You may also lose access to verified certificates and other program credentials like MicroMasters certificates. If you want to make a copy of these for your records before proceeding with deletion, {actionLink}.",
"account.settings.message.demographics.service.issue": "An error occurred attempting to retrieve or save your account information. Please try again later.",
"account.settings.field.demographics.gender": "Gender identity",
"account.settings.field.demographics.gender.empty": "Add gender identity",
"account.settings.field.demographics.gender.options.empty": "Select a gender identity",
"account.settings.field.demographics.gender.options.woman": "Woman",
"account.settings.field.demographics.gender.options.man": "Man",
"account.settings.field.demographics.gender.options.non-binary": "Non-binary",
"account.settings.field.demographics.gender.options.self-describe": "Prefer to self-describe",
"account.settings.field.demographics.gender_description": "Gender identity description",
"account.settings.field.demographics.gender_description.empty": "Enter description",
"account.settings.field.demographics.ethnicity": "Race/Ethnicity identity",
"account.settings.field.demographics.ethnicity.empty": "Add race/ethnicity identity",
"account.settings.field.demographics.ethnicity.options.empty": "Select all that apply",
"account.settings.field.demographics.ethnicity.options.american-indian-or-alaska-native": "American Indian or Alaska Native",
"account.settings.field.demographics.ethnicity.options.asian": "Asian",
"account.settings.field.demographics.ethnicity.options.black-or-african-american": "Black or African American",
"account.settings.field.demographics.ethnicity.options.hispanic-latin-spanish": "Hispanic, Latin, or Spanish origin",
"account.settings.field.demographics.ethnicity.options.middle-eastern-or-north-african": "Middle Eastern or North African",
"account.settings.field.demographics.ethnicity.options.native-hawaiian-or-pacific-islander": "Native Hawaiian or Other Pacific Islander",
"account.settings.field.demographics.ethnicity.options.white": "White",
"account.settings.field.demographics.ethnicity.options.other": "Some other race, ethnicity, or origin",
"account.settings.field.demographics.income": "Family income",
"account.settings.field.demographics.income.empty": "Add family income",
"account.settings.field.demographics.income.options.empty": "Select a family income range",
"account.settings.field.demographics.income.options.less-than-10k": "Less than US $10,000",
"account.settings.field.demographics.income.options.10k-25k": "US $10,000 - $25,000",
"account.settings.field.demographics.income.options.25k-50k": "US $25,000 - $50,000",
"account.settings.field.demographics.income.options.50k-75k": "US $50,000 - $75,000",
"account.settings.field.demographics.income.options.over-100k": "Over US $100,000",
"account.settings.field.demographics.income.options.unsure": "I don't know",
"account.settings.field.demographics.military_history": "U.S. Military status",
"account.settings.field.demographics.military_history.empty": "Add military status",
"account.settings.field.demographics.military_history.options.empty": "Select military status",
"account.settings.field.demographics.income.options.never-served": "Never served in the military",
"account.settings.field.demographics.income.options.training": "Only on active duty for training",
"account.settings.field.demographics.income.options.active": "Now on active duty",
"account.settings.field.demographics.income.options.previously-active": "On active duty in the past, but not now",
"account.settings.field.demographics.learner_education_level": "Your education level",
"account.settings.field.demographics.learner_education_level.empty": "Add education level",
"account.settings.field.demographics.parent_education_level": "Parents/Guardians education level",
"account.settings.field.demographics.parent_education_level.empty": "Add education level",
"account.settings.field.demographics.education_level.options.empty": "Select education level",
"account.settings.field.demographics.education_level.options.no-high-school": "No High School",
"account.settings.field.demographics.education_level.options.some-high-school": "Some High School",
"account.settings.field.demographics.education_level.options.high-school-ged-equivalent": "High School diploma, GED, or equivalent",
"account.settings.field.demographics.education_level.options.some-college": "Some college, but no degree",
"account.settings.field.demographics.education_level.options.associates": "Associates degree",
"account.settings.field.demographics.education_level.options.bachelors": "Bachelors degree",
"account.settings.field.demographics.education_level.options.masters": "Masters degree",
"account.settings.field.demographics.education_level.options.professional": "Professional degree",
"account.settings.field.demographics.education_level.options.doctorate": "Doctorate degree",
"account.settings.field.demographics.work_status": "Employment status",
"account.settings.field.demographics.work_status.empty": "Add employment status",
"account.settings.field.demographics.work_status.options.empty": "Select employment status",
"account.settings.field.demographics.work_status.options.full-time": "Employed, working full-time",
"account.settings.field.demographics.work_status.options.part-time": "Employed, working part-time",
"account.settings.field.demographics.work_status.options.not-employed-looking": "Not employed, looking for work",
"account.settings.field.demographics.work_status.options.not-employed-not-looking": "Not employed, not looking for work",
"account.settings.field.demographics.work_status.options.unable": "Unable to work",
"account.settings.field.demographics.work_status.options.retired": "Retired",
"account.settings.field.demographics.work_status.options.other": "Other",
"account.settings.field.demographics.work_status_description": "Employment status description",
"account.settings.field.demographics.work_status_description.empty": "Enter description",
"account.settings.field.demographics.current_work_sector": "Current work industry",
@@ -190,27 +153,7 @@
"account.settings.field.demographics.future_work_sector": "Future work industry",
"account.settings.field.demographics.future_work_sector.empty": "Add work industry",
"account.settings.field.demographics.work_sector.options.empty": "Select work industry",
"account.settings.field.demographics.work_sector.options.accommodation-food": "Accommodation and Food Services",
"account.settings.field.demographics.work_sector.options.administrative-support-waste-remediation": "Administrative and Support and Waste Management and Remediation Services",
"account.settings.field.demographics.work_sector.options.agriculture-forestry-fishing-hunting": "Agriculture, Forestry, Fishing and Hunting",
"account.settings.field.demographics.work_sector.options.arts-entertainment-recreation": "Arts, Entertainment, and Recreation",
"account.settings.field.demographics.work_sector.options.construction": "Construction",
"account.settings.field.demographics.work_sector.options.educational": "Education Services",
"account.settings.field.demographics.work_sector.options.finance-insurance": "Finance and Insurance",
"account.settings.field.demographics.work_sector.options.healthcare-social": "Health Care and Social Assistance",
"account.settings.field.demographics.work_sector.options.information": "Information",
"account.settings.field.demographics.work_sector.options.management": "Management of Companies and Enterprises",
"account.settings.field.demographics.work_sector.options.manufacturing": "Manufacturing",
"account.settings.field.demographics.work_sector.options.mining-quarry-oil-gas": "Mining, Quarrying, and Oil and Gas Extraction",
"account.settings.field.demographics.work_sector.options.professional-scientific-technical": "Professional, Scientific, and Technical Services",
"account.settings.field.demographics.work_sector.options.public-admin": "Public Administration",
"account.settings.field.demographics.work_sector.options.real-estate": "Real Estate and Rental and Leasing",
"account.settings.field.demographics.work_sector.options.retail": "Retail Trade",
"account.settings.field.demographics.work_sector.options.transport-warehousing": "Transportation and Warehousing",
"account.settings.field.demographics.work_sector.options.utilities": "Utilities",
"account.settings.field.demographics.work_sector.options.trade": "Wholesale Trade",
"account.settings.field.demographics.work_sector.options.other": "Other",
"account.settings.field.demographics.options.declined": "Prefer not to respond",
"account.settings.section.demographics.why": "Why does edX collect this information?",
"error.notfound.message": "The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again.",
"account.settings.editable.field.password.reset.button.confirmation.support.link": "technical support",
"account.settings.editable.field.password.reset.button.confirmation": "We've sent a message to {email}. Click the link in the message to reset your password. Didn't receive the message? Contact {technicalSupportLink}.",
@@ -222,6 +165,7 @@
"account.settings.sso.account.disconnect.error": "There was a problem disconnecting this account. Contact support if the problem persists.",
"account.settings.sso.unlink.account": "Unlink {name} account",
"account.settings.sso.no.providers": "No accounts can be linked at this time.",
"id.verification.existing.request.denied.text": "You cannot verify your identity at this time. If you have yet to activate your account, please check your spam folder for the activation email from {email}.",
"id.verification.next": "Next",
"id.verification.requirements.title": "Photo Verification Requirements",
"id.verification.requirements.description": "In order to complete Photo Verification online, you will need the following:",
@@ -235,49 +179,98 @@
"id.verification.privacy.do.with.photo.question": "What does edX do with this photo?",
"id.verification.privacy.do.with.photo.answer": "We securely encrypt your photo and send it our authorization service for review. Your photo and information are not saved or visible anywhere on edX after the verification process is complete.",
"id.verification.existing.request.title": "Identity Verification",
"id.verification.existing.request.pending.text": "You have already submitted your verification information. You will see a message on your dashboard when the verification process is complete (usually within 1-2 days).",
"id.verification.existing.request.denied.text": "You cannot verify your identity at this time.",
"id.verification.existing.request.pending.text": "You have already submitted your verification information. You will see a message on your dashboard when the verification process is complete (usually within 5 days).",
"id.verification.photo.take": "Take Photo",
"id.verification.photo.retake": "Retake Photo",
"id.verification.photo.enable.detection": "Enable Face Detection",
"id.verification.photo.enable.detection.portrait.help.text": "If checked, a box will appear around your face. Your face can be seen clearly if the box around it is blue. If your face is not in a good position or undetectable, the box will be red.",
"id.verification.photo.enable.detection.id.help.text": "If checked, a box will appear around the face on your ID card. The face can be seen clearly if the box around it is blue. If the face is not in a good position or undetectable, the box will be red.",
"id.verification.photo.feedback.correct": "Face is in a good position.",
"id.verification.photo.feedback.two.faces": "More than one face detected.",
"id.verification.photo.feedback.no.faces": "No face detected.",
"id.verification.photo.feedback.top.left": "Incorrect position. Top left.",
"id.verification.photo.feedback.top.center": "Incorrect position. Top center.",
"id.verification.photo.feedback.top.right": "Incorrect position. Top right.",
"id.verification.photo.feedback.center.left": "Incorrect position. Center left.",
"id.verification.photo.feedback.center.center": "Incorrect position. Too close to camera.",
"id.verification.photo.feedback.center.right": "Incorrect position. Center right.",
"id.verification.photo.feedback.bottom.left": "Incorrect position. Bottom left.",
"id.verification.photo.feedback.bottom.center": "Incorrect position. Bottom center.",
"id.verification.photo.feedback.bottom.right": "Incorrect position. Bottom right.",
"id.verification.camera.access.title": "Camera Permissions",
"id.verification.camera.access.title.success": "Camera Access Enabled",
"id.verification.camera.access.title.failed": "Camera Access Failed",
"id.verification.camera.access.click.allow": "Please make sure to click \"Allow\"",
"id.verification.camera.access.enable": "Enable Camera",
"id.verification.camera.access.problems": "Having problems?",
"id.verification.camera.access.skip": "Skip and upload image files instead",
"id.verification.camera.access.success": "Looks like your camera is working and ready.",
"id.verification.camera.access.failure": "It looks like we're unable to access your camera. You will need to upload image files of you and your photo id.",
"id.verification.camera.access.failure.temporary": "It looks like we're unable to access your camera. Please verify that your webcam is connected and that you have allowed your browser to access it.",
"id.verification.camera.access.failure.temporary.chrome": "To enable camera access in Chrome:",
"id.verification.camera.access.failure.temporary.chrome.step1": "Open Chrome.",
"id.verification.camera.access.failure.temporary.chrome.step2": "Navigate to More > Settings.",
"id.verification.camera.access.failure.temporary.chrome.step2.windows": "For Windows: Alt+F, Alt+E, or F10 followed by the spacebar",
"id.verification.camera.access.failure.temporary.chrome.step2.mac": "For Mac: Command+,",
"id.verification.camera.access.failure.temporary.chrome.step3": "Under the \"Privacy and security\" tab, select \"Site Settings\" and then \"Camera.\"",
"id.verification.camera.access.failure.temporary.chrome.step4": "Under \"Blocked,\" find \"edx.org\" and select it.",
"id.verification.camera.access.failure.temporary.chrome.step5": "In the \"Permissions\" section, update the camera permissions to \"Allow.\"",
"id.verification.camera.access.failure.temporary.ie11": "To enable camera access in Internet Explorer:",
"id.verification.camera.access.failure.temporary.ie11.step1": "Open the Flash Player Settings Manager by navigating to Windows Settings > Control Panel > Flash Player.",
"id.verification.camera.access.failure.temporary.ie11.step2": "Select the \"Camera and Mic\" tab, and then select the \"Camera and Microphone Settings by Site\" button.",
"id.verification.camera.access.failure.temporary.ie11.step3": "Choose \"edx.org\" from the list of websites and change the permissions by selecting \"Allow\" in the dropdown menu.",
"id.verification.camera.access.failure.temporary.firefox": "To enable camera access in Firefox:",
"id.verification.camera.access.failure.temporary.firefox.step1": "Open Firefox.",
"id.verification.camera.access.failure.temporary.firefox.step2": "Enter \"about:preferences\" in the URL bar.",
"id.verification.camera.access.failure.temporary.firefox.step3": "Select the \"Privacy & Security\" tab, and navigate to the \"Permissions\" section.",
"id.verification.camera.access.failure.temporary.firefox.step4": "Next to \"Camera,\" select the \"Settings…\" button.",
"id.verification.camera.access.failure.temporary.firefox.step5": "In the search bar, enter \"edx.org.\"",
"id.verification.camera.access.failure.temporary.firefox.step6": "In the status column for \"edx.org,\" select \"Allow\" from the drop down.",
"id.verification.camera.access.failure.temporary.firefox.step7": "Select \"Save Changes.\"",
"id.verification.camera.access.failure.temporary.safari": "To enable camera access in Safari:",
"id.verification.camera.access.failure.temporary.safari.step1": "Open Safari.",
"id.verification.camera.access.failure.temporary.safari.step2": "Click on the Safari app menu, then select \"Preferences.\" You can also use Command+, as a keyboard shortcut.",
"id.verification.camera.access.failure.temporary.safari.step3": "Select the \"Websites\" tab and then select \"Camera.\"",
"id.verification.camera.access.failure.temporary.safari.step4": "Select \"edx.org\" and change the camera permissions to \"Allow.\"",
"id.verification.photo.tips.title": "Helpful Photo Tips",
"id.verification.photo.tips.description": "Next, we'll need you to take a photo of your face. Please review the helpful tips below.",
"id.verification.photo.tips.list.title": "Photo Tips",
"id.verification.photo.tips.list.description": "To take a successful photo, make sure that:",
"id.verification.photo.tips.list.well.lit": "Your face is well-lit.",
"id.verification.photo.tips.list.inside.frame": "Your entire face fits inside the frame.",
"id.verification.portrait.photo.title.camera": "Take Your Photo",
"id.verification.portrait.photo.title.upload": "Upload Your Portrait Photo",
"id.verification.portrait.photo.title.camera": "Take a Photo of Yourself",
"id.verification.portrait.photo.title.upload": "Upload a Photo of Yourself",
"id.verification.portrait.photo.preview.alt": "Preview of photo of user's face.",
"id.verification.portrait.photo.instructions.camera": "When your face is in position, use the Take Photo button below to take your photo.",
"id.verification.portrait.photo.instructions.upload": "Please upload a portrait photo. Ensure your entire face fits inside the frame and is well-lit. (Supported formats: .jpg, .jpeg, .png)",
"id.verification.camera.help.sight.question": "What if I can't see the camera image or if I can't see my photo to determine which side is visible?",
"id.verification.camera.help.sight.answer": "You may be able to complete the image capture procedure without assistance, but it may take a couple of submission attempts to get the camera positioning right. Optimal camera positioning varies with each computer, but generally the best position for a headshot is approximately 12-18 inches (30-45 centimeters) from the camera, with your head centered relative to the computer screen. If the photos you submit are rejected, try moving the computer or camera orientation to change the lighting angle. The most common reason for rejection is inability to read the text on the ID card.",
"id.verification.camera.help.head.question": "What if I have difficulty holding my head in position relative to the camera?",
"id.verification.camera.help.head.answer": "If you require assistance with taking a photo for submission, contact edX support for additional suggestions.",
"id.verification.camera.help.sight.answer.portrait": "You may be able to complete the image capture procedure without assistance, but it may take a couple of submission attempts to get the camera positioning right. Optimal camera positioning varies with each computer, but generally the best position for a headshot is approximately 12-18 inches (30-45 centimeters) from the camera, with your head centered relative to the computer screen. If the photos you submit are rejected, try moving the computer or camera orientation to change the lighting angle.",
"id.verification.camera.help.sight.answer.id": "You may be able to complete the image capture procedure without assistance, but it may take a couple of submission attempts to get the camera positioning right. Optimal camera positioning varies with each computer, but generally, the best position for a photo of an ID card is 8-12 inches (20-30 centimeters) from the camera, with the ID card centered relative to the camera. If the photos you submit are rejected, try moving the computer or camera orientation to change the lighting angle. The most common reason for rejection is inability to read the text on the ID card.",
"id.verification.camera.help.difficulty.question.portrait": "What if I have difficulty holding my head in position relative to the camera?",
"id.verification.camera.help.difficulty.question.id": "What if I have difficulty holding my ID in position relative to the camera?",
"id.verification.camera.help.difficulty.answer": "If you require assistance with taking a photo for submission, contact edX support for additional suggestions.",
"id.verification.id.photo.unclear.question": "Is your ID image not clear or too blurry?",
"id.verification.id.tips.title": "Helpful ID Tips",
"id.verification.id.tips.description": "Next you'll need an eligible ID photo, make sure that:",
"id.verification.id.tips.description": "Next, we'll need you to take a photo of a valid ID that includes your name. Please have your ID ready.",
"id.verification.id.tips.list.well.lit": "Your ID is well-lit.",
"id.verification.id.tips.list.clear": "Ensure that you can see your photo and clearly read your name.",
"id.verification.id.photo.title.camera": "Take ID Photo",
"id.verification.id.photo.title.upload": "Upload Your ID Photo",
"id.verification.id.photo.title.camera": "Take a Photo of Your ID",
"id.verification.id.photo.title.upload": "Upload a Photo of Your ID",
"id.verification.id.photo.preview.alt": "Preview of photo ID.",
"id.verification.id.photo.instructions.camera": "When your ID is in position, use the Take Photo button below to take your photo.",
"id.verification.id.photo.instructions.upload": "Please upload an ID photo. Ensure the entire ID fits inside the frame and is well-lit. (Supported formats: .jpg, .jpeg, .png)",
"id.verification.id.photo.instructions.upload": "Please upload an ID photo. Ensure the entire ID fits inside the frame and is well-lit. The file size must be under 10 MB. (Supported formats: .jpg, .jpeg, .png)",
"id.verification.id.photo.instructions.upload.error": "The file you have selected is too large. Please try again with a file less than 10MB.",
"id.verification.account.name.title": "Account Name Check",
"id.verification.account.name.instructions": "Please check the Account Name below to ensure it matches the name on your ID. If not, click \"Edit\".",
"id.verification.account.name.instructions": "The name on your account and the name on your ID must be an exact match. If not, please click \"No\" to update your account name.",
"id.verification.account.name.radio.label": "Does the name on your ID match the Account Name below?",
"id.verification.account.name.radio.yes": "Yes",
"id.verification.account.name.radio.no": "No",
"id.verification.account.name.error": "Please update account name to match the name on your ID.",
"id.verification.account.name.warning.prefix": "Please Note:",
"id.verification.account.name.settings": "Account Settings",
"id.verification.account.name.label": "Name on ID",
"id.verification.account.name.edit": "Edit",
"id.verification.account.name.label": "Account Name",
"id.verification.account.name.photo.alt": "Photo of your ID to be submitted.",
"id.verification.account.name.save": "Save",
"id.verification.account.name.save": "Save and Next",
"id.verification.review.title": "Review Your Photos",
"id.verification.review.description": "Make sure we can verify your identity with the photos and information you have provided.",
"id.verification.review.portrait.label": "Your Portrait",
@@ -286,11 +279,14 @@
"id.verification.review.id.label": "Your Photo ID",
"id.verification.review.id.alt": "Photo of your ID to be submitted.",
"id.verification.review.id.retake": "Retake ID Photo",
"id.verification.review.confirm": "Confirm",
"id.verification.review.confirm": "Submit",
"id.verification.review.error": "edX Support Page",
"id.verification.submitted.title": "Identity Verification in Progress",
"id.verification.submitted.text": "We have received your information and are verifying your identity. You will see a message on your dashboard when the verification process is complete (usually within 1-2 days). In the meantime, you can still access all available course content.",
"id.verification.submitted.return": "Return to Your Dashboard",
"id.verification.account.name.warning": "{prefix} Any edit to your name will be saved to your account and can be reviewed on {accountSettings}.",
"id.verification.submitted.text": "We have received your information and are verifying your identity. You will see a message on your dashboard when the verification process is complete (usually within 5 days). In the meantime, you can still access all available course content.",
"id.verification.return.dashboard": "Return to Your Dashboard",
"id.verification.return.course": "Return to Course",
"id.verification.request.camera.access.instructions": "In order to take a photo using your webcam, you may receive a browser prompt for access to your camera. {clickAllow}",
"id.verification.requirements.card.device.text": "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}."
"id.verification.requirements.card.device.text": "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}.",
"idv.submission.alert.error": "\n We encountered a technical error while trying to submit ID verification.\n This might be a temporary issue, so please try again in a few minutes.\n If the problem persists,\n please go to {support_link} for help.\n ",
"id.verification.account.name.edit": "Edit{sr}"
}

View File

@@ -11,7 +11,7 @@
"account.settings.section.account.information": "Información de la cuenta",
"account.settings.section.account.information.description": "Estas configuraciones incluyen información básica sobre tu cuenta.",
"account.settings.section.profile.information": "Información del perfil",
"account.settings.section.demographics.information": "Optional Information",
"account.settings.section.demographics.information": "Información opcional",
"account.settings.section.site.preferences": "Preferencias del sitio",
"account.settings.section.linked.accounts": "Cuentas vinculadas",
"account.settings.section.linked.accounts.description": "Puedes vincular tus cuentas de redes sociales para simplificar el proceso de iniciar sesión en edX.",
@@ -117,100 +117,43 @@
"account.settings.delete.account.modal.confirm.cancel": "Cancelar",
"account.settings.delete.account.error.unable.to.delete": "No es posible eliminar esta cuenta",
"account.settings.delete.account.error.no.password": "Se requiere una contraseña",
"account.settings.delete.account.error.invalid.password": "Password is incorrect",
"account.settings.delete.account.error.invalid.password": "Contraseña incorrecta",
"account.settings.delete.account.error.unable.to.delete.details": "Ocurrió un error al procesar tu solicitud. Por favor, intente nuevamente más tarde.",
"account.settings.delete.account.modal.after.header": "¡Sentimos que te vayas! Tu cuenta será eliminada en breve.",
"account.settings.delete.account.modal.after.text": "La eliminación de cuenta, incluyendo la eliminación de las listas de correo electrónico, puede tardar unas semanas en procesarse totalmente en nuestro sistema. Si quieres renunciar a recibir correos antes de que la eliminación se haya completado, por favor date de baja mediante el enlace que aparece al final de los correos.",
"account.settings.delete.account.modal.after.button": "Cerrar",
"account.settings.delete.account.text.3": "Puede que también pierdas el acceso a los certificados verificados y otros certificados de programas como los de los MicroMasters. Si quieres hacer una copia de dichos certificados para tus archivos antes de proceder a la eliminación, {actionLink}.",
"account.settings.field.demographics.gender": "Gender identity",
"account.settings.field.demographics.gender.empty": "Add gender identity",
"account.settings.field.demographics.gender.options.empty": "Select a gender identity",
"account.settings.field.demographics.gender.options.woman": "Woman",
"account.settings.field.demographics.gender.options.man": "Man",
"account.settings.field.demographics.gender.options.non-binary": "Non-binary",
"account.settings.field.demographics.gender.options.self-describe": "Prefer to self-describe",
"account.settings.field.demographics.gender_description": "Gender identity description",
"account.settings.field.demographics.gender_description.empty": "Enter description",
"account.settings.field.demographics.ethnicity": "Race/Ethnicity identity",
"account.settings.field.demographics.ethnicity.empty": "Add race/ethnicity identity",
"account.settings.field.demographics.ethnicity.options.empty": "Select all that apply",
"account.settings.field.demographics.ethnicity.options.american-indian-or-alaska-native": "American Indian or Alaska Native",
"account.settings.field.demographics.ethnicity.options.asian": "Asian",
"account.settings.field.demographics.ethnicity.options.black-or-african-american": "Black or African American",
"account.settings.field.demographics.ethnicity.options.hispanic-latin-spanish": "Hispanic, Latin, or Spanish origin",
"account.settings.field.demographics.ethnicity.options.middle-eastern-or-north-african": "Middle Eastern or North African",
"account.settings.field.demographics.ethnicity.options.native-hawaiian-or-pacific-islander": "Native Hawaiian or Other Pacific Islander",
"account.settings.field.demographics.ethnicity.options.white": "White",
"account.settings.field.demographics.ethnicity.options.other": "Some other race, ethnicity, or origin",
"account.settings.field.demographics.income": "Family income",
"account.settings.field.demographics.income.empty": "Add family income",
"account.settings.field.demographics.income.options.empty": "Select a family income range",
"account.settings.field.demographics.income.options.less-than-10k": "Less than US $10,000",
"account.settings.field.demographics.income.options.10k-25k": "US $10,000 - $25,000",
"account.settings.field.demographics.income.options.25k-50k": "US $25,000 - $50,000",
"account.settings.field.demographics.income.options.50k-75k": "US $50,000 - $75,000",
"account.settings.field.demographics.income.options.over-100k": "Over US $100,000",
"account.settings.field.demographics.income.options.unsure": "I don't know",
"account.settings.field.demographics.military_history": "U.S. Military status",
"account.settings.message.demographics.service.issue": "Ocurrió un error al intentar recuperar o guardar la información de tu cuenta. Por favor inténtalo más tarde.",
"account.settings.field.demographics.gender": "Identidad de género",
"account.settings.field.demographics.gender.empty": "Añade identidad de género",
"account.settings.field.demographics.gender.options.empty": "Selecciona una identidad de género",
"account.settings.field.demographics.gender_description": "Descripción de identidad de género",
"account.settings.field.demographics.gender_description.empty": "Ingresa descripción",
"account.settings.field.demographics.ethnicity": "Identidad étnica/raza",
"account.settings.field.demographics.ethnicity.empty": "Añade identidad étnica/raza",
"account.settings.field.demographics.ethnicity.options.empty": "Selecciona todas las que apliquen",
"account.settings.field.demographics.income": "Ingreso familiar",
"account.settings.field.demographics.income.empty": "Añade ingreso familiar",
"account.settings.field.demographics.income.options.empty": "Selecciona un rango de ingreso familiar",
"account.settings.field.demographics.military_history": "Estatus militar en EE.UU.",
"account.settings.field.demographics.military_history.empty": "Add military status",
"account.settings.field.demographics.military_history.options.empty": "Select military status",
"account.settings.field.demographics.income.options.never-served": "Never served in the military",
"account.settings.field.demographics.income.options.training": "Only on active duty for training",
"account.settings.field.demographics.income.options.active": "Now on active duty",
"account.settings.field.demographics.income.options.previously-active": "On active duty in the past, but not now",
"account.settings.field.demographics.learner_education_level": "Your education level",
"account.settings.field.demographics.learner_education_level.empty": "Add education level",
"account.settings.field.demographics.parent_education_level": "Parents/Guardians education level",
"account.settings.field.demographics.parent_education_level.empty": "Add education level",
"account.settings.field.demographics.education_level.options.empty": "Select education level",
"account.settings.field.demographics.education_level.options.no-high-school": "No High School",
"account.settings.field.demographics.education_level.options.some-high-school": "Some High School",
"account.settings.field.demographics.education_level.options.high-school-ged-equivalent": "High School diploma, GED, or equivalent",
"account.settings.field.demographics.education_level.options.some-college": "Some college, but no degree",
"account.settings.field.demographics.education_level.options.associates": "Associates degree",
"account.settings.field.demographics.education_level.options.bachelors": "Bachelors degree",
"account.settings.field.demographics.education_level.options.masters": "Masters degree",
"account.settings.field.demographics.education_level.options.professional": "Professional degree",
"account.settings.field.demographics.education_level.options.doctorate": "Doctorate degree",
"account.settings.field.demographics.work_status": "Employment status",
"account.settings.field.demographics.work_status.empty": "Add employment status",
"account.settings.field.demographics.work_status.options.empty": "Select employment status",
"account.settings.field.demographics.work_status.options.full-time": "Employed, working full-time",
"account.settings.field.demographics.work_status.options.part-time": "Employed, working part-time",
"account.settings.field.demographics.work_status.options.not-employed-looking": "Not employed, looking for work",
"account.settings.field.demographics.work_status.options.not-employed-not-looking": "Not employed, not looking for work",
"account.settings.field.demographics.work_status.options.unable": "Unable to work",
"account.settings.field.demographics.work_status.options.retired": "Retired",
"account.settings.field.demographics.work_status.options.other": "Other",
"account.settings.field.demographics.work_status_description": "Employment status description",
"account.settings.field.demographics.work_status_description.empty": "Enter description",
"account.settings.field.demographics.current_work_sector": "Current work industry",
"account.settings.field.demographics.current_work_sector.empty": "Add work industry",
"account.settings.field.demographics.future_work_sector": "Future work industry",
"account.settings.field.demographics.future_work_sector.empty": "Add work industry",
"account.settings.field.demographics.work_sector.options.empty": "Select work industry",
"account.settings.field.demographics.work_sector.options.accommodation-food": "Accommodation and Food Services",
"account.settings.field.demographics.work_sector.options.administrative-support-waste-remediation": "Administrative and Support and Waste Management and Remediation Services",
"account.settings.field.demographics.work_sector.options.agriculture-forestry-fishing-hunting": "Agriculture, Forestry, Fishing and Hunting",
"account.settings.field.demographics.work_sector.options.arts-entertainment-recreation": "Arts, Entertainment, and Recreation",
"account.settings.field.demographics.work_sector.options.construction": "Construction",
"account.settings.field.demographics.work_sector.options.educational": "Education Services",
"account.settings.field.demographics.work_sector.options.finance-insurance": "Finance and Insurance",
"account.settings.field.demographics.work_sector.options.healthcare-social": "Health Care and Social Assistance",
"account.settings.field.demographics.work_sector.options.information": "Information",
"account.settings.field.demographics.work_sector.options.management": "Management of Companies and Enterprises",
"account.settings.field.demographics.work_sector.options.manufacturing": "Manufacturing",
"account.settings.field.demographics.work_sector.options.mining-quarry-oil-gas": "Mining, Quarrying, and Oil and Gas Extraction",
"account.settings.field.demographics.work_sector.options.professional-scientific-technical": "Professional, Scientific, and Technical Services",
"account.settings.field.demographics.work_sector.options.public-admin": "Public Administration",
"account.settings.field.demographics.work_sector.options.real-estate": "Real Estate and Rental and Leasing",
"account.settings.field.demographics.work_sector.options.retail": "Retail Trade",
"account.settings.field.demographics.work_sector.options.transport-warehousing": "Transportation and Warehousing",
"account.settings.field.demographics.work_sector.options.utilities": "Utilities",
"account.settings.field.demographics.work_sector.options.trade": "Wholesale Trade",
"account.settings.field.demographics.work_sector.options.other": "Other",
"account.settings.field.demographics.options.declined": "Prefer not to respond",
"account.settings.field.demographics.military_history.options.empty": "Selecciona estatus militar",
"account.settings.field.demographics.learner_education_level": "Tu nivel educacional",
"account.settings.field.demographics.learner_education_level.empty": "Añade nivel educacional",
"account.settings.field.demographics.parent_education_level": "Nivel educacional de padres/tutores",
"account.settings.field.demographics.parent_education_level.empty": "Añade nivel educacional",
"account.settings.field.demographics.education_level.options.empty": "Selecciona nivel educacional",
"account.settings.field.demographics.work_status": "Estatus laboral",
"account.settings.field.demographics.work_status.empty": "Añade estatus laboral",
"account.settings.field.demographics.work_status.options.empty": "Selecciona estatus laboral",
"account.settings.field.demographics.work_status_description": "Descripción estatus laboral",
"account.settings.field.demographics.work_status_description.empty": "Ingresa descripción",
"account.settings.field.demographics.current_work_sector": "Área profesional actual",
"account.settings.field.demographics.current_work_sector.empty": "Añade área profesional",
"account.settings.field.demographics.future_work_sector": "Área profesional futura",
"account.settings.field.demographics.future_work_sector.empty": "Añade área profesional",
"account.settings.field.demographics.work_sector.options.empty": "Selecciona área profesional",
"account.settings.section.demographics.why": "¿Por qué edX obtiene esta información?",
"error.notfound.message": "La página que estas buscando no está disponible o hay un error en la URL. Por favor, comprueba la URL y vuelve a intentarlo.",
"account.settings.editable.field.password.reset.button.confirmation.support.link": "soporte técnico",
"account.settings.editable.field.password.reset.button.confirmation": "Hemos mandado un mensaje a {email}. Haz clic en el enlace en el mensaje para restablecer tu contraseña. ¿No recibiste el mensaje? Contáctate con {technicalSupportLink}.",
@@ -222,62 +165,112 @@
"account.settings.sso.account.disconnect.error": "Hubo un problema al desconectar esta Cuenta. Si el problema persiste, contacte soporte.",
"account.settings.sso.unlink.account": "Desvincular la cuenta de {accountName} ",
"account.settings.sso.no.providers": "No se pueden vincular cuentas en este momento.",
"id.verification.next": "Next",
"id.verification.requirements.title": "Photo Verification Requirements",
"id.verification.requirements.description": "In order to complete Photo Verification online, you will need the following:",
"id.verification.requirements.card.device.title": "Device with Camera",
"id.verification.requirements.card.device.allow": "Allow",
"id.verification.requirements.card.id.title": "Photo Identification",
"id.verification.requirements.card.id.text": "You need a valid ID that contains your full name and photo.",
"id.verification.existing.request.denied.text": "You cannot verify your identity at this time. If you have yet to activate your account, please check your spam folder for the activation email from {email}.",
"id.verification.next": "Siguiente",
"id.verification.requirements.title": "Requerimientos de verificación por foto",
"id.verification.requirements.description": "Para completar la verificación por foto en línea, necesitarás lo siguiente:",
"id.verification.requirements.card.device.title": "Dispositivo con cámara",
"id.verification.requirements.card.device.allow": "Permitir",
"id.verification.requirements.card.id.title": "Identificación por foto",
"id.verification.requirements.card.id.text": "Necesitas un ID válido que contenga tu nombre completo y tu foto.",
"id.verification.privacy.title": "Privacy Information",
"id.verification.privacy.need.photo.question": "Why does edX need my photo?",
"id.verification.privacy.need.photo.answer": "We use your verification photos to confirm your identity and ensure the validity of your certificate.",
"id.verification.privacy.do.with.photo.question": "What does edX do with this photo?",
"id.verification.privacy.need.photo.question": "¿Por qué edX necesita mi foto?",
"id.verification.privacy.need.photo.answer": "Utilizamos tus fotos de verificación para confirmar tu identidad y garantizar la validez de tu certificado.",
"id.verification.privacy.do.with.photo.question": "¿Qué hace edX con esta foto?",
"id.verification.privacy.do.with.photo.answer": "We securely encrypt your photo and send it our authorization service for review. Your photo and information are not saved or visible anywhere on edX after the verification process is complete.",
"id.verification.existing.request.title": "Identity Verification",
"id.verification.existing.request.pending.text": "You have already submitted your verification information. You will see a message on your dashboard when the verification process is complete (usually within 1-2 days).",
"id.verification.existing.request.denied.text": "You cannot verify your identity at this time.",
"id.verification.photo.take": "Take Photo",
"id.verification.photo.retake": "Retake Photo",
"id.verification.camera.access.title": "Camera Permissions",
"id.verification.camera.access.click.allow": "Please make sure to click \"Allow\"",
"id.verification.camera.access.enable": "Enable Camera",
"id.verification.camera.access.problems": "Having problems?",
"id.verification.camera.access.skip": "Skip and upload image files instead",
"id.verification.camera.access.success": "Looks like your camera is working and ready.",
"id.verification.existing.request.title": "Verificación de identidad",
"id.verification.existing.request.pending.text": "You have already submitted your verification information. You will see a message on your dashboard when the verification process is complete (usually within 5 days).",
"id.verification.photo.take": "Tomar la foto",
"id.verification.photo.retake": "Tomar nuevamente la foto",
"id.verification.photo.enable.detection": "Enable Face Detection",
"id.verification.photo.enable.detection.portrait.help.text": "If checked, a box will appear around your face. Your face can be seen clearly if the box around it is blue. If your face is not in a good position or undetectable, the box will be red.",
"id.verification.photo.enable.detection.id.help.text": "If checked, a box will appear around the face on your ID card. The face can be seen clearly if the box around it is blue. If the face is not in a good position or undetectable, the box will be red.",
"id.verification.photo.feedback.correct": "Face is in a good position.",
"id.verification.photo.feedback.two.faces": "More than one face detected.",
"id.verification.photo.feedback.no.faces": "No face detected.",
"id.verification.photo.feedback.top.left": "Incorrect position. Top left.",
"id.verification.photo.feedback.top.center": "Incorrect position. Top center.",
"id.verification.photo.feedback.top.right": "Incorrect position. Top right.",
"id.verification.photo.feedback.center.left": "Incorrect position. Center left.",
"id.verification.photo.feedback.center.center": "Incorrect position. Too close to camera.",
"id.verification.photo.feedback.center.right": "Incorrect position. Center right.",
"id.verification.photo.feedback.bottom.left": "Incorrect position. Bottom left.",
"id.verification.photo.feedback.bottom.center": "Incorrect position. Bottom center.",
"id.verification.photo.feedback.bottom.right": "Incorrect position. Bottom right.",
"id.verification.camera.access.title": "Permisos de la cámara",
"id.verification.camera.access.title.success": "Camera Access Enabled",
"id.verification.camera.access.title.failed": "Camera Access Failed",
"id.verification.camera.access.click.allow": "Por favor asegúrate de hacer clic en \"Permitir\"",
"id.verification.camera.access.enable": "Habilitar cámara",
"id.verification.camera.access.problems": "¿Tienes problemas?",
"id.verification.camera.access.skip": "Omitir y cargar un archivo de imagen",
"id.verification.camera.access.success": "Parece que tu cámara está funcionando y está lista.",
"id.verification.camera.access.failure": "It looks like we're unable to access your camera. You will need to upload image files of you and your photo id.",
"id.verification.photo.tips.title": "Helpful Photo Tips",
"id.verification.camera.access.failure.temporary": "It looks like we're unable to access your camera. Please verify that your webcam is connected and that you have allowed your browser to access it.",
"id.verification.camera.access.failure.temporary.chrome": "To enable camera access in Chrome:",
"id.verification.camera.access.failure.temporary.chrome.step1": "Open Chrome.",
"id.verification.camera.access.failure.temporary.chrome.step2": "Navigate to More > Settings.",
"id.verification.camera.access.failure.temporary.chrome.step2.windows": "For Windows: Alt+F, Alt+E, or F10 followed by the spacebar",
"id.verification.camera.access.failure.temporary.chrome.step2.mac": "For Mac: Command+,",
"id.verification.camera.access.failure.temporary.chrome.step3": "Under the \"Privacy and security\" tab, select \"Site Settings\" and then \"Camera.\"",
"id.verification.camera.access.failure.temporary.chrome.step4": "Under \"Blocked,\" find \"edx.org\" and select it.",
"id.verification.camera.access.failure.temporary.chrome.step5": "In the \"Permissions\" section, update the camera permissions to \"Allow.\"",
"id.verification.camera.access.failure.temporary.ie11": "To enable camera access in Internet Explorer:",
"id.verification.camera.access.failure.temporary.ie11.step1": "Open the Flash Player Settings Manager by navigating to Windows Settings > Control Panel > Flash Player.",
"id.verification.camera.access.failure.temporary.ie11.step2": "Select the \"Camera and Mic\" tab, and then select the \"Camera and Microphone Settings by Site\" button.",
"id.verification.camera.access.failure.temporary.ie11.step3": "Choose \"edx.org\" from the list of websites and change the permissions by selecting \"Allow\" in the dropdown menu.",
"id.verification.camera.access.failure.temporary.firefox": "To enable camera access in Firefox:",
"id.verification.camera.access.failure.temporary.firefox.step1": "Open Firefox.",
"id.verification.camera.access.failure.temporary.firefox.step2": "Enter \"about:preferences\" in the URL bar.",
"id.verification.camera.access.failure.temporary.firefox.step3": "Select the \"Privacy & Security\" tab, and navigate to the \"Permissions\" section.",
"id.verification.camera.access.failure.temporary.firefox.step4": "Next to \"Camera,\" select the \"Settings…\" button.",
"id.verification.camera.access.failure.temporary.firefox.step5": "In the search bar, enter \"edx.org.\"",
"id.verification.camera.access.failure.temporary.firefox.step6": "In the status column for \"edx.org,\" select \"Allow\" from the drop down.",
"id.verification.camera.access.failure.temporary.firefox.step7": "Select \"Save Changes.\"",
"id.verification.camera.access.failure.temporary.safari": "To enable camera access in Safari:",
"id.verification.camera.access.failure.temporary.safari.step1": "Open Safari.",
"id.verification.camera.access.failure.temporary.safari.step2": "Click on the Safari app menu, then select \"Preferences.\" You can also use Command+, as a keyboard shortcut.",
"id.verification.camera.access.failure.temporary.safari.step3": "Select the \"Websites\" tab and then select \"Camera.\"",
"id.verification.camera.access.failure.temporary.safari.step4": "Select \"edx.org\" and change the camera permissions to \"Allow.\"",
"id.verification.photo.tips.title": "Consejos útiles de fotos",
"id.verification.photo.tips.description": "Next, we'll need you to take a photo of your face. Please review the helpful tips below.",
"id.verification.photo.tips.list.title": "Photo Tips",
"id.verification.photo.tips.list.description": "To take a successful photo, make sure that:",
"id.verification.photo.tips.list.well.lit": "Your face is well-lit.",
"id.verification.photo.tips.list.inside.frame": "Your entire face fits inside the frame.",
"id.verification.portrait.photo.title.camera": "Take Your Photo",
"id.verification.portrait.photo.title.upload": "Upload Your Portrait Photo",
"id.verification.portrait.photo.title.camera": "Take a Photo of Yourself",
"id.verification.portrait.photo.title.upload": "Upload a Photo of Yourself",
"id.verification.portrait.photo.preview.alt": "Preview of photo of user's face.",
"id.verification.portrait.photo.instructions.camera": "When your face is in position, use the Take Photo button below to take your photo.",
"id.verification.portrait.photo.instructions.upload": "Please upload a portrait photo. Ensure your entire face fits inside the frame and is well-lit. (Supported formats: .jpg, .jpeg, .png)",
"id.verification.camera.help.sight.question": "What if I can't see the camera image or if I can't see my photo to determine which side is visible?",
"id.verification.camera.help.sight.answer": "You may be able to complete the image capture procedure without assistance, but it may take a couple of submission attempts to get the camera positioning right. Optimal camera positioning varies with each computer, but generally the best position for a headshot is approximately 12-18 inches (30-45 centimeters) from the camera, with your head centered relative to the computer screen. If the photos you submit are rejected, try moving the computer or camera orientation to change the lighting angle. The most common reason for rejection is inability to read the text on the ID card.",
"id.verification.camera.help.head.question": "What if I have difficulty holding my head in position relative to the camera?",
"id.verification.camera.help.head.answer": "If you require assistance with taking a photo for submission, contact edX support for additional suggestions.",
"id.verification.camera.help.sight.answer.portrait": "You may be able to complete the image capture procedure without assistance, but it may take a couple of submission attempts to get the camera positioning right. Optimal camera positioning varies with each computer, but generally the best position for a headshot is approximately 12-18 inches (30-45 centimeters) from the camera, with your head centered relative to the computer screen. If the photos you submit are rejected, try moving the computer or camera orientation to change the lighting angle.",
"id.verification.camera.help.sight.answer.id": "You may be able to complete the image capture procedure without assistance, but it may take a couple of submission attempts to get the camera positioning right. Optimal camera positioning varies with each computer, but generally, the best position for a photo of an ID card is 8-12 inches (20-30 centimeters) from the camera, with the ID card centered relative to the camera. If the photos you submit are rejected, try moving the computer or camera orientation to change the lighting angle. The most common reason for rejection is inability to read the text on the ID card.",
"id.verification.camera.help.difficulty.question.portrait": "What if I have difficulty holding my head in position relative to the camera?",
"id.verification.camera.help.difficulty.question.id": "What if I have difficulty holding my ID in position relative to the camera?",
"id.verification.camera.help.difficulty.answer": "If you require assistance with taking a photo for submission, contact edX support for additional suggestions.",
"id.verification.id.photo.unclear.question": "Is your ID image not clear or too blurry?",
"id.verification.id.tips.title": "Helpful ID Tips",
"id.verification.id.tips.description": "Next you'll need an eligible ID photo, make sure that:",
"id.verification.id.tips.description": "Next, we'll need you to take a photo of a valid ID that includes your name. Please have your ID ready.",
"id.verification.id.tips.list.well.lit": "Your ID is well-lit.",
"id.verification.id.tips.list.clear": "Ensure that you can see your photo and clearly read your name.",
"id.verification.id.photo.title.camera": "Take ID Photo",
"id.verification.id.photo.title.upload": "Upload Your ID Photo",
"id.verification.id.photo.title.camera": "Take a Photo of Your ID",
"id.verification.id.photo.title.upload": "Upload a Photo of Your ID",
"id.verification.id.photo.preview.alt": "Preview of photo ID.",
"id.verification.id.photo.instructions.camera": "When your ID is in position, use the Take Photo button below to take your photo.",
"id.verification.id.photo.instructions.upload": "Please upload an ID photo. Ensure the entire ID fits inside the frame and is well-lit. (Supported formats: .jpg, .jpeg, .png)",
"id.verification.id.photo.instructions.upload": "Please upload an ID photo. Ensure the entire ID fits inside the frame and is well-lit. The file size must be under 10 MB. (Supported formats: .jpg, .jpeg, .png)",
"id.verification.id.photo.instructions.upload.error": "The file you have selected is too large. Please try again with a file less than 10MB.",
"id.verification.account.name.title": "Account Name Check",
"id.verification.account.name.instructions": "Please check the Account Name below to ensure it matches the name on your ID. If not, click \"Edit\".",
"id.verification.account.name.instructions": "The name on your account and the name on your ID must be an exact match. If not, please click \"No\" to update your account name.",
"id.verification.account.name.radio.label": "Does the name on your ID match the Account Name below?",
"id.verification.account.name.radio.yes": "Yes",
"id.verification.account.name.radio.no": "No",
"id.verification.account.name.error": "Please update account name to match the name on your ID.",
"id.verification.account.name.warning.prefix": "Please Note:",
"id.verification.account.name.settings": "Account Settings",
"id.verification.account.name.label": "Name on ID",
"id.verification.account.name.edit": "Edit",
"id.verification.account.name.label": "Account Name",
"id.verification.account.name.photo.alt": "Photo of your ID to be submitted.",
"id.verification.account.name.save": "Save",
"id.verification.account.name.save": "Save and Next",
"id.verification.review.title": "Review Your Photos",
"id.verification.review.description": "Make sure we can verify your identity with the photos and information you have provided.",
"id.verification.review.portrait.label": "Your Portrait",
@@ -286,11 +279,14 @@
"id.verification.review.id.label": "Your Photo ID",
"id.verification.review.id.alt": "Photo of your ID to be submitted.",
"id.verification.review.id.retake": "Retake ID Photo",
"id.verification.review.confirm": "Confirm",
"id.verification.review.confirm": "Submit",
"id.verification.review.error": "edX Support Page",
"id.verification.submitted.title": "Identity Verification in Progress",
"id.verification.submitted.text": "We have received your information and are verifying your identity. You will see a message on your dashboard when the verification process is complete (usually within 1-2 days). In the meantime, you can still access all available course content.",
"id.verification.submitted.return": "Return to Your Dashboard",
"id.verification.account.name.warning": "{prefix} Any edit to your name will be saved to your account and can be reviewed on {accountSettings}.",
"id.verification.submitted.text": "We have received your information and are verifying your identity. You will see a message on your dashboard when the verification process is complete (usually within 5 days). In the meantime, you can still access all available course content.",
"id.verification.return.dashboard": "Return to Your Dashboard",
"id.verification.return.course": "Return to Course",
"id.verification.request.camera.access.instructions": "In order to take a photo using your webcam, you may receive a browser prompt for access to your camera. {clickAllow}",
"id.verification.requirements.card.device.text": "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}."
"id.verification.requirements.card.device.text": "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}.",
"idv.submission.alert.error": "\n We encountered a technical error while trying to submit ID verification.\n This might be a temporary issue, so please try again in a few minutes.\n If the problem persists,\n please go to {support_link} for help.\n ",
"id.verification.account.name.edit": "Edit{sr}"
}

View File

@@ -123,66 +123,29 @@
"account.settings.delete.account.modal.after.text": "Account deletion, including removal from email lists, may take a few weeks to fully process through our system. If you want to opt-out of emails before then, please unsubscribe from the footer of any email.",
"account.settings.delete.account.modal.after.button": "Close",
"account.settings.delete.account.text.3": "You may also lose access to verified certificates and other program credentials like MicroMasters certificates. If you want to make a copy of these for your records before proceeding with deletion, {actionLink}.",
"account.settings.message.demographics.service.issue": "An error occurred attempting to retrieve or save your account information. Please try again later.",
"account.settings.field.demographics.gender": "Gender identity",
"account.settings.field.demographics.gender.empty": "Add gender identity",
"account.settings.field.demographics.gender.options.empty": "Select a gender identity",
"account.settings.field.demographics.gender.options.woman": "Woman",
"account.settings.field.demographics.gender.options.man": "Man",
"account.settings.field.demographics.gender.options.non-binary": "Non-binary",
"account.settings.field.demographics.gender.options.self-describe": "Prefer to self-describe",
"account.settings.field.demographics.gender_description": "Gender identity description",
"account.settings.field.demographics.gender_description.empty": "Enter description",
"account.settings.field.demographics.ethnicity": "Race/Ethnicity identity",
"account.settings.field.demographics.ethnicity.empty": "Add race/ethnicity identity",
"account.settings.field.demographics.ethnicity.options.empty": "Select all that apply",
"account.settings.field.demographics.ethnicity.options.american-indian-or-alaska-native": "American Indian or Alaska Native",
"account.settings.field.demographics.ethnicity.options.asian": "Asian",
"account.settings.field.demographics.ethnicity.options.black-or-african-american": "Black or African American",
"account.settings.field.demographics.ethnicity.options.hispanic-latin-spanish": "Hispanic, Latin, or Spanish origin",
"account.settings.field.demographics.ethnicity.options.middle-eastern-or-north-african": "Middle Eastern or North African",
"account.settings.field.demographics.ethnicity.options.native-hawaiian-or-pacific-islander": "Native Hawaiian or Other Pacific Islander",
"account.settings.field.demographics.ethnicity.options.white": "White",
"account.settings.field.demographics.ethnicity.options.other": "Some other race, ethnicity, or origin",
"account.settings.field.demographics.income": "Family income",
"account.settings.field.demographics.income.empty": "Add family income",
"account.settings.field.demographics.income.options.empty": "Select a family income range",
"account.settings.field.demographics.income.options.less-than-10k": "Less than US $10,000",
"account.settings.field.demographics.income.options.10k-25k": "US $10,000 - $25,000",
"account.settings.field.demographics.income.options.25k-50k": "US $25,000 - $50,000",
"account.settings.field.demographics.income.options.50k-75k": "US $50,000 - $75,000",
"account.settings.field.demographics.income.options.over-100k": "Over US $100,000",
"account.settings.field.demographics.income.options.unsure": "I don't know",
"account.settings.field.demographics.military_history": "U.S. Military status",
"account.settings.field.demographics.military_history.empty": "Add military status",
"account.settings.field.demographics.military_history.options.empty": "Select military status",
"account.settings.field.demographics.income.options.never-served": "Never served in the military",
"account.settings.field.demographics.income.options.training": "Only on active duty for training",
"account.settings.field.demographics.income.options.active": "Now on active duty",
"account.settings.field.demographics.income.options.previously-active": "On active duty in the past, but not now",
"account.settings.field.demographics.learner_education_level": "Your education level",
"account.settings.field.demographics.learner_education_level.empty": "Add education level",
"account.settings.field.demographics.parent_education_level": "Parents/Guardians education level",
"account.settings.field.demographics.parent_education_level.empty": "Add education level",
"account.settings.field.demographics.education_level.options.empty": "Select education level",
"account.settings.field.demographics.education_level.options.no-high-school": "No High School",
"account.settings.field.demographics.education_level.options.some-high-school": "Some High School",
"account.settings.field.demographics.education_level.options.high-school-ged-equivalent": "High School diploma, GED, or equivalent",
"account.settings.field.demographics.education_level.options.some-college": "Some college, but no degree",
"account.settings.field.demographics.education_level.options.associates": "Associates degree",
"account.settings.field.demographics.education_level.options.bachelors": "Bachelors degree",
"account.settings.field.demographics.education_level.options.masters": "Masters degree",
"account.settings.field.demographics.education_level.options.professional": "Professional degree",
"account.settings.field.demographics.education_level.options.doctorate": "Doctorate degree",
"account.settings.field.demographics.work_status": "Employment status",
"account.settings.field.demographics.work_status.empty": "Add employment status",
"account.settings.field.demographics.work_status.options.empty": "Select employment status",
"account.settings.field.demographics.work_status.options.full-time": "Employed, working full-time",
"account.settings.field.demographics.work_status.options.part-time": "Employed, working part-time",
"account.settings.field.demographics.work_status.options.not-employed-looking": "Not employed, looking for work",
"account.settings.field.demographics.work_status.options.not-employed-not-looking": "Not employed, not looking for work",
"account.settings.field.demographics.work_status.options.unable": "Unable to work",
"account.settings.field.demographics.work_status.options.retired": "Retired",
"account.settings.field.demographics.work_status.options.other": "Other",
"account.settings.field.demographics.work_status_description": "Employment status description",
"account.settings.field.demographics.work_status_description.empty": "Enter description",
"account.settings.field.demographics.current_work_sector": "Current work industry",
@@ -190,27 +153,7 @@
"account.settings.field.demographics.future_work_sector": "Future work industry",
"account.settings.field.demographics.future_work_sector.empty": "Add work industry",
"account.settings.field.demographics.work_sector.options.empty": "Select work industry",
"account.settings.field.demographics.work_sector.options.accommodation-food": "Accommodation and Food Services",
"account.settings.field.demographics.work_sector.options.administrative-support-waste-remediation": "Administrative and Support and Waste Management and Remediation Services",
"account.settings.field.demographics.work_sector.options.agriculture-forestry-fishing-hunting": "Agriculture, Forestry, Fishing and Hunting",
"account.settings.field.demographics.work_sector.options.arts-entertainment-recreation": "Arts, Entertainment, and Recreation",
"account.settings.field.demographics.work_sector.options.construction": "Construction",
"account.settings.field.demographics.work_sector.options.educational": "Education Services",
"account.settings.field.demographics.work_sector.options.finance-insurance": "Finance and Insurance",
"account.settings.field.demographics.work_sector.options.healthcare-social": "Health Care and Social Assistance",
"account.settings.field.demographics.work_sector.options.information": "Information",
"account.settings.field.demographics.work_sector.options.management": "Management of Companies and Enterprises",
"account.settings.field.demographics.work_sector.options.manufacturing": "Manufacturing",
"account.settings.field.demographics.work_sector.options.mining-quarry-oil-gas": "Mining, Quarrying, and Oil and Gas Extraction",
"account.settings.field.demographics.work_sector.options.professional-scientific-technical": "Professional, Scientific, and Technical Services",
"account.settings.field.demographics.work_sector.options.public-admin": "Public Administration",
"account.settings.field.demographics.work_sector.options.real-estate": "Real Estate and Rental and Leasing",
"account.settings.field.demographics.work_sector.options.retail": "Retail Trade",
"account.settings.field.demographics.work_sector.options.transport-warehousing": "Transportation and Warehousing",
"account.settings.field.demographics.work_sector.options.utilities": "Utilities",
"account.settings.field.demographics.work_sector.options.trade": "Wholesale Trade",
"account.settings.field.demographics.work_sector.options.other": "Other",
"account.settings.field.demographics.options.declined": "Prefer not to respond",
"account.settings.section.demographics.why": "Why does edX collect this information?",
"error.notfound.message": "The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again.",
"account.settings.editable.field.password.reset.button.confirmation.support.link": "technical support",
"account.settings.editable.field.password.reset.button.confirmation": "We've sent a message to {email}. Click the link in the message to reset your password. Didn't receive the message? Contact {technicalSupportLink}.",
@@ -222,6 +165,7 @@
"account.settings.sso.account.disconnect.error": "There was a problem disconnecting this account. Contact support if the problem persists.",
"account.settings.sso.unlink.account": "Unlink {name} account",
"account.settings.sso.no.providers": "No accounts can be linked at this time.",
"id.verification.existing.request.denied.text": "You cannot verify your identity at this time. If you have yet to activate your account, please check your spam folder for the activation email from {email}.",
"id.verification.next": "Next",
"id.verification.requirements.title": "Photo Verification Requirements",
"id.verification.requirements.description": "In order to complete Photo Verification online, you will need the following:",
@@ -235,49 +179,98 @@
"id.verification.privacy.do.with.photo.question": "What does edX do with this photo?",
"id.verification.privacy.do.with.photo.answer": "We securely encrypt your photo and send it our authorization service for review. Your photo and information are not saved or visible anywhere on edX after the verification process is complete.",
"id.verification.existing.request.title": "Identity Verification",
"id.verification.existing.request.pending.text": "You have already submitted your verification information. You will see a message on your dashboard when the verification process is complete (usually within 1-2 days).",
"id.verification.existing.request.denied.text": "You cannot verify your identity at this time.",
"id.verification.existing.request.pending.text": "You have already submitted your verification information. You will see a message on your dashboard when the verification process is complete (usually within 5 days).",
"id.verification.photo.take": "Take Photo",
"id.verification.photo.retake": "Retake Photo",
"id.verification.photo.enable.detection": "Enable Face Detection",
"id.verification.photo.enable.detection.portrait.help.text": "If checked, a box will appear around your face. Your face can be seen clearly if the box around it is blue. If your face is not in a good position or undetectable, the box will be red.",
"id.verification.photo.enable.detection.id.help.text": "If checked, a box will appear around the face on your ID card. The face can be seen clearly if the box around it is blue. If the face is not in a good position or undetectable, the box will be red.",
"id.verification.photo.feedback.correct": "Face is in a good position.",
"id.verification.photo.feedback.two.faces": "More than one face detected.",
"id.verification.photo.feedback.no.faces": "No face detected.",
"id.verification.photo.feedback.top.left": "Incorrect position. Top left.",
"id.verification.photo.feedback.top.center": "Incorrect position. Top center.",
"id.verification.photo.feedback.top.right": "Incorrect position. Top right.",
"id.verification.photo.feedback.center.left": "Incorrect position. Center left.",
"id.verification.photo.feedback.center.center": "Incorrect position. Too close to camera.",
"id.verification.photo.feedback.center.right": "Incorrect position. Center right.",
"id.verification.photo.feedback.bottom.left": "Incorrect position. Bottom left.",
"id.verification.photo.feedback.bottom.center": "Incorrect position. Bottom center.",
"id.verification.photo.feedback.bottom.right": "Incorrect position. Bottom right.",
"id.verification.camera.access.title": "Camera Permissions",
"id.verification.camera.access.title.success": "Camera Access Enabled",
"id.verification.camera.access.title.failed": "Camera Access Failed",
"id.verification.camera.access.click.allow": "Please make sure to click \"Allow\"",
"id.verification.camera.access.enable": "Enable Camera",
"id.verification.camera.access.problems": "Having problems?",
"id.verification.camera.access.skip": "Skip and upload image files instead",
"id.verification.camera.access.success": "Looks like your camera is working and ready.",
"id.verification.camera.access.failure": "It looks like we're unable to access your camera. You will need to upload image files of you and your photo id.",
"id.verification.camera.access.failure.temporary": "It looks like we're unable to access your camera. Please verify that your webcam is connected and that you have allowed your browser to access it.",
"id.verification.camera.access.failure.temporary.chrome": "To enable camera access in Chrome:",
"id.verification.camera.access.failure.temporary.chrome.step1": "Open Chrome.",
"id.verification.camera.access.failure.temporary.chrome.step2": "Navigate to More > Settings.",
"id.verification.camera.access.failure.temporary.chrome.step2.windows": "For Windows: Alt+F, Alt+E, or F10 followed by the spacebar",
"id.verification.camera.access.failure.temporary.chrome.step2.mac": "For Mac: Command+,",
"id.verification.camera.access.failure.temporary.chrome.step3": "Under the \"Privacy and security\" tab, select \"Site Settings\" and then \"Camera.\"",
"id.verification.camera.access.failure.temporary.chrome.step4": "Under \"Blocked,\" find \"edx.org\" and select it.",
"id.verification.camera.access.failure.temporary.chrome.step5": "In the \"Permissions\" section, update the camera permissions to \"Allow.\"",
"id.verification.camera.access.failure.temporary.ie11": "To enable camera access in Internet Explorer:",
"id.verification.camera.access.failure.temporary.ie11.step1": "Open the Flash Player Settings Manager by navigating to Windows Settings > Control Panel > Flash Player.",
"id.verification.camera.access.failure.temporary.ie11.step2": "Select the \"Camera and Mic\" tab, and then select the \"Camera and Microphone Settings by Site\" button.",
"id.verification.camera.access.failure.temporary.ie11.step3": "Choose \"edx.org\" from the list of websites and change the permissions by selecting \"Allow\" in the dropdown menu.",
"id.verification.camera.access.failure.temporary.firefox": "To enable camera access in Firefox:",
"id.verification.camera.access.failure.temporary.firefox.step1": "Open Firefox.",
"id.verification.camera.access.failure.temporary.firefox.step2": "Enter \"about:preferences\" in the URL bar.",
"id.verification.camera.access.failure.temporary.firefox.step3": "Select the \"Privacy & Security\" tab, and navigate to the \"Permissions\" section.",
"id.verification.camera.access.failure.temporary.firefox.step4": "Next to \"Camera,\" select the \"Settings…\" button.",
"id.verification.camera.access.failure.temporary.firefox.step5": "In the search bar, enter \"edx.org.\"",
"id.verification.camera.access.failure.temporary.firefox.step6": "In the status column for \"edx.org,\" select \"Allow\" from the drop down.",
"id.verification.camera.access.failure.temporary.firefox.step7": "Select \"Save Changes.\"",
"id.verification.camera.access.failure.temporary.safari": "To enable camera access in Safari:",
"id.verification.camera.access.failure.temporary.safari.step1": "Open Safari.",
"id.verification.camera.access.failure.temporary.safari.step2": "Click on the Safari app menu, then select \"Preferences.\" You can also use Command+, as a keyboard shortcut.",
"id.verification.camera.access.failure.temporary.safari.step3": "Select the \"Websites\" tab and then select \"Camera.\"",
"id.verification.camera.access.failure.temporary.safari.step4": "Select \"edx.org\" and change the camera permissions to \"Allow.\"",
"id.verification.photo.tips.title": "Helpful Photo Tips",
"id.verification.photo.tips.description": "Next, we'll need you to take a photo of your face. Please review the helpful tips below.",
"id.verification.photo.tips.list.title": "Photo Tips",
"id.verification.photo.tips.list.description": "To take a successful photo, make sure that:",
"id.verification.photo.tips.list.well.lit": "Your face is well-lit.",
"id.verification.photo.tips.list.inside.frame": "Your entire face fits inside the frame.",
"id.verification.portrait.photo.title.camera": "Take Your Photo",
"id.verification.portrait.photo.title.upload": "Upload Your Portrait Photo",
"id.verification.portrait.photo.title.camera": "Take a Photo of Yourself",
"id.verification.portrait.photo.title.upload": "Upload a Photo of Yourself",
"id.verification.portrait.photo.preview.alt": "Preview of photo of user's face.",
"id.verification.portrait.photo.instructions.camera": "When your face is in position, use the Take Photo button below to take your photo.",
"id.verification.portrait.photo.instructions.upload": "Please upload a portrait photo. Ensure your entire face fits inside the frame and is well-lit. (Supported formats: .jpg, .jpeg, .png)",
"id.verification.camera.help.sight.question": "What if I can't see the camera image or if I can't see my photo to determine which side is visible?",
"id.verification.camera.help.sight.answer": "You may be able to complete the image capture procedure without assistance, but it may take a couple of submission attempts to get the camera positioning right. Optimal camera positioning varies with each computer, but generally the best position for a headshot is approximately 12-18 inches (30-45 centimeters) from the camera, with your head centered relative to the computer screen. If the photos you submit are rejected, try moving the computer or camera orientation to change the lighting angle. The most common reason for rejection is inability to read the text on the ID card.",
"id.verification.camera.help.head.question": "What if I have difficulty holding my head in position relative to the camera?",
"id.verification.camera.help.head.answer": "If you require assistance with taking a photo for submission, contact edX support for additional suggestions.",
"id.verification.camera.help.sight.answer.portrait": "You may be able to complete the image capture procedure without assistance, but it may take a couple of submission attempts to get the camera positioning right. Optimal camera positioning varies with each computer, but generally the best position for a headshot is approximately 12-18 inches (30-45 centimeters) from the camera, with your head centered relative to the computer screen. If the photos you submit are rejected, try moving the computer or camera orientation to change the lighting angle.",
"id.verification.camera.help.sight.answer.id": "You may be able to complete the image capture procedure without assistance, but it may take a couple of submission attempts to get the camera positioning right. Optimal camera positioning varies with each computer, but generally, the best position for a photo of an ID card is 8-12 inches (20-30 centimeters) from the camera, with the ID card centered relative to the camera. If the photos you submit are rejected, try moving the computer or camera orientation to change the lighting angle. The most common reason for rejection is inability to read the text on the ID card.",
"id.verification.camera.help.difficulty.question.portrait": "What if I have difficulty holding my head in position relative to the camera?",
"id.verification.camera.help.difficulty.question.id": "What if I have difficulty holding my ID in position relative to the camera?",
"id.verification.camera.help.difficulty.answer": "If you require assistance with taking a photo for submission, contact edX support for additional suggestions.",
"id.verification.id.photo.unclear.question": "Is your ID image not clear or too blurry?",
"id.verification.id.tips.title": "Helpful ID Tips",
"id.verification.id.tips.description": "Next you'll need an eligible ID photo, make sure that:",
"id.verification.id.tips.description": "Next, we'll need you to take a photo of a valid ID that includes your name. Please have your ID ready.",
"id.verification.id.tips.list.well.lit": "Your ID is well-lit.",
"id.verification.id.tips.list.clear": "Ensure that you can see your photo and clearly read your name.",
"id.verification.id.photo.title.camera": "Take ID Photo",
"id.verification.id.photo.title.upload": "Upload Your ID Photo",
"id.verification.id.photo.title.camera": "Take a Photo of Your ID",
"id.verification.id.photo.title.upload": "Upload a Photo of Your ID",
"id.verification.id.photo.preview.alt": "Preview of photo ID.",
"id.verification.id.photo.instructions.camera": "When your ID is in position, use the Take Photo button below to take your photo.",
"id.verification.id.photo.instructions.upload": "Please upload an ID photo. Ensure the entire ID fits inside the frame and is well-lit. (Supported formats: .jpg, .jpeg, .png)",
"id.verification.id.photo.instructions.upload": "Please upload an ID photo. Ensure the entire ID fits inside the frame and is well-lit. The file size must be under 10 MB. (Supported formats: .jpg, .jpeg, .png)",
"id.verification.id.photo.instructions.upload.error": "The file you have selected is too large. Please try again with a file less than 10MB.",
"id.verification.account.name.title": "Account Name Check",
"id.verification.account.name.instructions": "Please check the Account Name below to ensure it matches the name on your ID. If not, click \"Edit\".",
"id.verification.account.name.instructions": "The name on your account and the name on your ID must be an exact match. If not, please click \"No\" to update your account name.",
"id.verification.account.name.radio.label": "Does the name on your ID match the Account Name below?",
"id.verification.account.name.radio.yes": "Yes",
"id.verification.account.name.radio.no": "No",
"id.verification.account.name.error": "Please update account name to match the name on your ID.",
"id.verification.account.name.warning.prefix": "Please Note:",
"id.verification.account.name.settings": "Account Settings",
"id.verification.account.name.label": "Name on ID",
"id.verification.account.name.edit": "Edit",
"id.verification.account.name.label": "Account Name",
"id.verification.account.name.photo.alt": "Photo of your ID to be submitted.",
"id.verification.account.name.save": "Save",
"id.verification.account.name.save": "Save and Next",
"id.verification.review.title": "Review Your Photos",
"id.verification.review.description": "Make sure we can verify your identity with the photos and information you have provided.",
"id.verification.review.portrait.label": "Your Portrait",
@@ -286,11 +279,14 @@
"id.verification.review.id.label": "Your Photo ID",
"id.verification.review.id.alt": "Photo of your ID to be submitted.",
"id.verification.review.id.retake": "Retake ID Photo",
"id.verification.review.confirm": "Confirm",
"id.verification.review.confirm": "Submit",
"id.verification.review.error": "edX Support Page",
"id.verification.submitted.title": "Identity Verification in Progress",
"id.verification.submitted.text": "We have received your information and are verifying your identity. You will see a message on your dashboard when the verification process is complete (usually within 1-2 days). In the meantime, you can still access all available course content.",
"id.verification.submitted.return": "Return to Your Dashboard",
"id.verification.account.name.warning": "{prefix} Any edit to your name will be saved to your account and can be reviewed on {accountSettings}.",
"id.verification.submitted.text": "We have received your information and are verifying your identity. You will see a message on your dashboard when the verification process is complete (usually within 5 days). In the meantime, you can still access all available course content.",
"id.verification.return.dashboard": "Return to Your Dashboard",
"id.verification.return.course": "Return to Course",
"id.verification.request.camera.access.instructions": "In order to take a photo using your webcam, you may receive a browser prompt for access to your camera. {clickAllow}",
"id.verification.requirements.card.device.text": "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}."
"id.verification.requirements.card.device.text": "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}.",
"idv.submission.alert.error": "\n We encountered a technical error while trying to submit ID verification.\n This might be a temporary issue, so please try again in a few minutes.\n If the problem persists,\n please go to {support_link} for help.\n ",
"id.verification.account.name.edit": "Edit{sr}"
}

View File

@@ -123,66 +123,29 @@
"account.settings.delete.account.modal.after.text": "Account deletion, including removal from email lists, may take a few weeks to fully process through our system. If you want to opt-out of emails before then, please unsubscribe from the footer of any email.",
"account.settings.delete.account.modal.after.button": "Close",
"account.settings.delete.account.text.3": "You may also lose access to verified certificates and other program credentials like MicroMasters certificates. If you want to make a copy of these for your records before proceeding with deletion, {actionLink}.",
"account.settings.message.demographics.service.issue": "An error occurred attempting to retrieve or save your account information. Please try again later.",
"account.settings.field.demographics.gender": "Gender identity",
"account.settings.field.demographics.gender.empty": "Add gender identity",
"account.settings.field.demographics.gender.options.empty": "Select a gender identity",
"account.settings.field.demographics.gender.options.woman": "Woman",
"account.settings.field.demographics.gender.options.man": "Man",
"account.settings.field.demographics.gender.options.non-binary": "Non-binary",
"account.settings.field.demographics.gender.options.self-describe": "Prefer to self-describe",
"account.settings.field.demographics.gender_description": "Gender identity description",
"account.settings.field.demographics.gender_description.empty": "Enter description",
"account.settings.field.demographics.ethnicity": "Race/Ethnicity identity",
"account.settings.field.demographics.ethnicity.empty": "Add race/ethnicity identity",
"account.settings.field.demographics.ethnicity.options.empty": "Select all that apply",
"account.settings.field.demographics.ethnicity.options.american-indian-or-alaska-native": "American Indian or Alaska Native",
"account.settings.field.demographics.ethnicity.options.asian": "Asian",
"account.settings.field.demographics.ethnicity.options.black-or-african-american": "Black or African American",
"account.settings.field.demographics.ethnicity.options.hispanic-latin-spanish": "Hispanic, Latin, or Spanish origin",
"account.settings.field.demographics.ethnicity.options.middle-eastern-or-north-african": "Middle Eastern or North African",
"account.settings.field.demographics.ethnicity.options.native-hawaiian-or-pacific-islander": "Native Hawaiian or Other Pacific Islander",
"account.settings.field.demographics.ethnicity.options.white": "White",
"account.settings.field.demographics.ethnicity.options.other": "Some other race, ethnicity, or origin",
"account.settings.field.demographics.income": "Family income",
"account.settings.field.demographics.income.empty": "Add family income",
"account.settings.field.demographics.income.options.empty": "Select a family income range",
"account.settings.field.demographics.income.options.less-than-10k": "Less than US $10,000",
"account.settings.field.demographics.income.options.10k-25k": "US $10,000 - $25,000",
"account.settings.field.demographics.income.options.25k-50k": "US $25,000 - $50,000",
"account.settings.field.demographics.income.options.50k-75k": "US $50,000 - $75,000",
"account.settings.field.demographics.income.options.over-100k": "Over US $100,000",
"account.settings.field.demographics.income.options.unsure": "I don't know",
"account.settings.field.demographics.military_history": "U.S. Military status",
"account.settings.field.demographics.military_history.empty": "Add military status",
"account.settings.field.demographics.military_history.options.empty": "Select military status",
"account.settings.field.demographics.income.options.never-served": "Never served in the military",
"account.settings.field.demographics.income.options.training": "Only on active duty for training",
"account.settings.field.demographics.income.options.active": "Now on active duty",
"account.settings.field.demographics.income.options.previously-active": "On active duty in the past, but not now",
"account.settings.field.demographics.learner_education_level": "Your education level",
"account.settings.field.demographics.learner_education_level.empty": "Add education level",
"account.settings.field.demographics.parent_education_level": "Parents/Guardians education level",
"account.settings.field.demographics.parent_education_level.empty": "Add education level",
"account.settings.field.demographics.education_level.options.empty": "Select education level",
"account.settings.field.demographics.education_level.options.no-high-school": "No High School",
"account.settings.field.demographics.education_level.options.some-high-school": "Some High School",
"account.settings.field.demographics.education_level.options.high-school-ged-equivalent": "High School diploma, GED, or equivalent",
"account.settings.field.demographics.education_level.options.some-college": "Some college, but no degree",
"account.settings.field.demographics.education_level.options.associates": "Associates degree",
"account.settings.field.demographics.education_level.options.bachelors": "Bachelors degree",
"account.settings.field.demographics.education_level.options.masters": "Masters degree",
"account.settings.field.demographics.education_level.options.professional": "Professional degree",
"account.settings.field.demographics.education_level.options.doctorate": "Doctorate degree",
"account.settings.field.demographics.work_status": "Employment status",
"account.settings.field.demographics.work_status.empty": "Add employment status",
"account.settings.field.demographics.work_status.options.empty": "Select employment status",
"account.settings.field.demographics.work_status.options.full-time": "Employed, working full-time",
"account.settings.field.demographics.work_status.options.part-time": "Employed, working part-time",
"account.settings.field.demographics.work_status.options.not-employed-looking": "Not employed, looking for work",
"account.settings.field.demographics.work_status.options.not-employed-not-looking": "Not employed, not looking for work",
"account.settings.field.demographics.work_status.options.unable": "Unable to work",
"account.settings.field.demographics.work_status.options.retired": "Retired",
"account.settings.field.demographics.work_status.options.other": "Other",
"account.settings.field.demographics.work_status_description": "Employment status description",
"account.settings.field.demographics.work_status_description.empty": "Enter description",
"account.settings.field.demographics.current_work_sector": "Current work industry",
@@ -190,27 +153,7 @@
"account.settings.field.demographics.future_work_sector": "Future work industry",
"account.settings.field.demographics.future_work_sector.empty": "Add work industry",
"account.settings.field.demographics.work_sector.options.empty": "Select work industry",
"account.settings.field.demographics.work_sector.options.accommodation-food": "Accommodation and Food Services",
"account.settings.field.demographics.work_sector.options.administrative-support-waste-remediation": "Administrative and Support and Waste Management and Remediation Services",
"account.settings.field.demographics.work_sector.options.agriculture-forestry-fishing-hunting": "Agriculture, Forestry, Fishing and Hunting",
"account.settings.field.demographics.work_sector.options.arts-entertainment-recreation": "Arts, Entertainment, and Recreation",
"account.settings.field.demographics.work_sector.options.construction": "Construction",
"account.settings.field.demographics.work_sector.options.educational": "Education Services",
"account.settings.field.demographics.work_sector.options.finance-insurance": "Finance and Insurance",
"account.settings.field.demographics.work_sector.options.healthcare-social": "Health Care and Social Assistance",
"account.settings.field.demographics.work_sector.options.information": "Information",
"account.settings.field.demographics.work_sector.options.management": "Management of Companies and Enterprises",
"account.settings.field.demographics.work_sector.options.manufacturing": "Manufacturing",
"account.settings.field.demographics.work_sector.options.mining-quarry-oil-gas": "Mining, Quarrying, and Oil and Gas Extraction",
"account.settings.field.demographics.work_sector.options.professional-scientific-technical": "Professional, Scientific, and Technical Services",
"account.settings.field.demographics.work_sector.options.public-admin": "Public Administration",
"account.settings.field.demographics.work_sector.options.real-estate": "Real Estate and Rental and Leasing",
"account.settings.field.demographics.work_sector.options.retail": "Retail Trade",
"account.settings.field.demographics.work_sector.options.transport-warehousing": "Transportation and Warehousing",
"account.settings.field.demographics.work_sector.options.utilities": "Utilities",
"account.settings.field.demographics.work_sector.options.trade": "Wholesale Trade",
"account.settings.field.demographics.work_sector.options.other": "Other",
"account.settings.field.demographics.options.declined": "Prefer not to respond",
"account.settings.section.demographics.why": "Why does edX collect this information?",
"error.notfound.message": "The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again.",
"account.settings.editable.field.password.reset.button.confirmation.support.link": "technical support",
"account.settings.editable.field.password.reset.button.confirmation": "We've sent a message to {email}. Click the link in the message to reset your password. Didn't receive the message? Contact {technicalSupportLink}.",
@@ -222,6 +165,7 @@
"account.settings.sso.account.disconnect.error": "There was a problem disconnecting this account. Contact support if the problem persists.",
"account.settings.sso.unlink.account": "Unlink {name} account",
"account.settings.sso.no.providers": "No accounts can be linked at this time.",
"id.verification.existing.request.denied.text": "You cannot verify your identity at this time. If you have yet to activate your account, please check your spam folder for the activation email from {email}.",
"id.verification.next": "Next",
"id.verification.requirements.title": "Photo Verification Requirements",
"id.verification.requirements.description": "In order to complete Photo Verification online, you will need the following:",
@@ -235,49 +179,98 @@
"id.verification.privacy.do.with.photo.question": "What does edX do with this photo?",
"id.verification.privacy.do.with.photo.answer": "We securely encrypt your photo and send it our authorization service for review. Your photo and information are not saved or visible anywhere on edX after the verification process is complete.",
"id.verification.existing.request.title": "Identity Verification",
"id.verification.existing.request.pending.text": "You have already submitted your verification information. You will see a message on your dashboard when the verification process is complete (usually within 1-2 days).",
"id.verification.existing.request.denied.text": "You cannot verify your identity at this time.",
"id.verification.existing.request.pending.text": "You have already submitted your verification information. You will see a message on your dashboard when the verification process is complete (usually within 5 days).",
"id.verification.photo.take": "Take Photo",
"id.verification.photo.retake": "Retake Photo",
"id.verification.photo.enable.detection": "Enable Face Detection",
"id.verification.photo.enable.detection.portrait.help.text": "If checked, a box will appear around your face. Your face can be seen clearly if the box around it is blue. If your face is not in a good position or undetectable, the box will be red.",
"id.verification.photo.enable.detection.id.help.text": "If checked, a box will appear around the face on your ID card. The face can be seen clearly if the box around it is blue. If the face is not in a good position or undetectable, the box will be red.",
"id.verification.photo.feedback.correct": "Face is in a good position.",
"id.verification.photo.feedback.two.faces": "More than one face detected.",
"id.verification.photo.feedback.no.faces": "No face detected.",
"id.verification.photo.feedback.top.left": "Incorrect position. Top left.",
"id.verification.photo.feedback.top.center": "Incorrect position. Top center.",
"id.verification.photo.feedback.top.right": "Incorrect position. Top right.",
"id.verification.photo.feedback.center.left": "Incorrect position. Center left.",
"id.verification.photo.feedback.center.center": "Incorrect position. Too close to camera.",
"id.verification.photo.feedback.center.right": "Incorrect position. Center right.",
"id.verification.photo.feedback.bottom.left": "Incorrect position. Bottom left.",
"id.verification.photo.feedback.bottom.center": "Incorrect position. Bottom center.",
"id.verification.photo.feedback.bottom.right": "Incorrect position. Bottom right.",
"id.verification.camera.access.title": "Camera Permissions",
"id.verification.camera.access.title.success": "Camera Access Enabled",
"id.verification.camera.access.title.failed": "Camera Access Failed",
"id.verification.camera.access.click.allow": "Please make sure to click \"Allow\"",
"id.verification.camera.access.enable": "Enable Camera",
"id.verification.camera.access.problems": "Having problems?",
"id.verification.camera.access.skip": "Skip and upload image files instead",
"id.verification.camera.access.success": "Looks like your camera is working and ready.",
"id.verification.camera.access.failure": "It looks like we're unable to access your camera. You will need to upload image files of you and your photo id.",
"id.verification.camera.access.failure.temporary": "It looks like we're unable to access your camera. Please verify that your webcam is connected and that you have allowed your browser to access it.",
"id.verification.camera.access.failure.temporary.chrome": "To enable camera access in Chrome:",
"id.verification.camera.access.failure.temporary.chrome.step1": "Open Chrome.",
"id.verification.camera.access.failure.temporary.chrome.step2": "Navigate to More > Settings.",
"id.verification.camera.access.failure.temporary.chrome.step2.windows": "For Windows: Alt+F, Alt+E, or F10 followed by the spacebar",
"id.verification.camera.access.failure.temporary.chrome.step2.mac": "For Mac: Command+,",
"id.verification.camera.access.failure.temporary.chrome.step3": "Under the \"Privacy and security\" tab, select \"Site Settings\" and then \"Camera.\"",
"id.verification.camera.access.failure.temporary.chrome.step4": "Under \"Blocked,\" find \"edx.org\" and select it.",
"id.verification.camera.access.failure.temporary.chrome.step5": "In the \"Permissions\" section, update the camera permissions to \"Allow.\"",
"id.verification.camera.access.failure.temporary.ie11": "To enable camera access in Internet Explorer:",
"id.verification.camera.access.failure.temporary.ie11.step1": "Open the Flash Player Settings Manager by navigating to Windows Settings > Control Panel > Flash Player.",
"id.verification.camera.access.failure.temporary.ie11.step2": "Select the \"Camera and Mic\" tab, and then select the \"Camera and Microphone Settings by Site\" button.",
"id.verification.camera.access.failure.temporary.ie11.step3": "Choose \"edx.org\" from the list of websites and change the permissions by selecting \"Allow\" in the dropdown menu.",
"id.verification.camera.access.failure.temporary.firefox": "To enable camera access in Firefox:",
"id.verification.camera.access.failure.temporary.firefox.step1": "Open Firefox.",
"id.verification.camera.access.failure.temporary.firefox.step2": "Enter \"about:preferences\" in the URL bar.",
"id.verification.camera.access.failure.temporary.firefox.step3": "Select the \"Privacy & Security\" tab, and navigate to the \"Permissions\" section.",
"id.verification.camera.access.failure.temporary.firefox.step4": "Next to \"Camera,\" select the \"Settings…\" button.",
"id.verification.camera.access.failure.temporary.firefox.step5": "In the search bar, enter \"edx.org.\"",
"id.verification.camera.access.failure.temporary.firefox.step6": "In the status column for \"edx.org,\" select \"Allow\" from the drop down.",
"id.verification.camera.access.failure.temporary.firefox.step7": "Select \"Save Changes.\"",
"id.verification.camera.access.failure.temporary.safari": "To enable camera access in Safari:",
"id.verification.camera.access.failure.temporary.safari.step1": "Open Safari.",
"id.verification.camera.access.failure.temporary.safari.step2": "Click on the Safari app menu, then select \"Preferences.\" You can also use Command+, as a keyboard shortcut.",
"id.verification.camera.access.failure.temporary.safari.step3": "Select the \"Websites\" tab and then select \"Camera.\"",
"id.verification.camera.access.failure.temporary.safari.step4": "Select \"edx.org\" and change the camera permissions to \"Allow.\"",
"id.verification.photo.tips.title": "Helpful Photo Tips",
"id.verification.photo.tips.description": "Next, we'll need you to take a photo of your face. Please review the helpful tips below.",
"id.verification.photo.tips.list.title": "Photo Tips",
"id.verification.photo.tips.list.description": "To take a successful photo, make sure that:",
"id.verification.photo.tips.list.well.lit": "Your face is well-lit.",
"id.verification.photo.tips.list.inside.frame": "Your entire face fits inside the frame.",
"id.verification.portrait.photo.title.camera": "Take Your Photo",
"id.verification.portrait.photo.title.upload": "Upload Your Portrait Photo",
"id.verification.portrait.photo.title.camera": "Take a Photo of Yourself",
"id.verification.portrait.photo.title.upload": "Upload a Photo of Yourself",
"id.verification.portrait.photo.preview.alt": "Preview of photo of user's face.",
"id.verification.portrait.photo.instructions.camera": "When your face is in position, use the Take Photo button below to take your photo.",
"id.verification.portrait.photo.instructions.upload": "Please upload a portrait photo. Ensure your entire face fits inside the frame and is well-lit. (Supported formats: .jpg, .jpeg, .png)",
"id.verification.camera.help.sight.question": "What if I can't see the camera image or if I can't see my photo to determine which side is visible?",
"id.verification.camera.help.sight.answer": "You may be able to complete the image capture procedure without assistance, but it may take a couple of submission attempts to get the camera positioning right. Optimal camera positioning varies with each computer, but generally the best position for a headshot is approximately 12-18 inches (30-45 centimeters) from the camera, with your head centered relative to the computer screen. If the photos you submit are rejected, try moving the computer or camera orientation to change the lighting angle. The most common reason for rejection is inability to read the text on the ID card.",
"id.verification.camera.help.head.question": "What if I have difficulty holding my head in position relative to the camera?",
"id.verification.camera.help.head.answer": "If you require assistance with taking a photo for submission, contact edX support for additional suggestions.",
"id.verification.camera.help.sight.answer.portrait": "You may be able to complete the image capture procedure without assistance, but it may take a couple of submission attempts to get the camera positioning right. Optimal camera positioning varies with each computer, but generally the best position for a headshot is approximately 12-18 inches (30-45 centimeters) from the camera, with your head centered relative to the computer screen. If the photos you submit are rejected, try moving the computer or camera orientation to change the lighting angle.",
"id.verification.camera.help.sight.answer.id": "You may be able to complete the image capture procedure without assistance, but it may take a couple of submission attempts to get the camera positioning right. Optimal camera positioning varies with each computer, but generally, the best position for a photo of an ID card is 8-12 inches (20-30 centimeters) from the camera, with the ID card centered relative to the camera. If the photos you submit are rejected, try moving the computer or camera orientation to change the lighting angle. The most common reason for rejection is inability to read the text on the ID card.",
"id.verification.camera.help.difficulty.question.portrait": "What if I have difficulty holding my head in position relative to the camera?",
"id.verification.camera.help.difficulty.question.id": "What if I have difficulty holding my ID in position relative to the camera?",
"id.verification.camera.help.difficulty.answer": "If you require assistance with taking a photo for submission, contact edX support for additional suggestions.",
"id.verification.id.photo.unclear.question": "Is your ID image not clear or too blurry?",
"id.verification.id.tips.title": "Helpful ID Tips",
"id.verification.id.tips.description": "Next you'll need an eligible ID photo, make sure that:",
"id.verification.id.tips.description": "Next, we'll need you to take a photo of a valid ID that includes your name. Please have your ID ready.",
"id.verification.id.tips.list.well.lit": "Your ID is well-lit.",
"id.verification.id.tips.list.clear": "Ensure that you can see your photo and clearly read your name.",
"id.verification.id.photo.title.camera": "Take ID Photo",
"id.verification.id.photo.title.upload": "Upload Your ID Photo",
"id.verification.id.photo.title.camera": "Take a Photo of Your ID",
"id.verification.id.photo.title.upload": "Upload a Photo of Your ID",
"id.verification.id.photo.preview.alt": "Preview of photo ID.",
"id.verification.id.photo.instructions.camera": "When your ID is in position, use the Take Photo button below to take your photo.",
"id.verification.id.photo.instructions.upload": "Please upload an ID photo. Ensure the entire ID fits inside the frame and is well-lit. (Supported formats: .jpg, .jpeg, .png)",
"id.verification.id.photo.instructions.upload": "Please upload an ID photo. Ensure the entire ID fits inside the frame and is well-lit. The file size must be under 10 MB. (Supported formats: .jpg, .jpeg, .png)",
"id.verification.id.photo.instructions.upload.error": "The file you have selected is too large. Please try again with a file less than 10MB.",
"id.verification.account.name.title": "Account Name Check",
"id.verification.account.name.instructions": "Please check the Account Name below to ensure it matches the name on your ID. If not, click \"Edit\".",
"id.verification.account.name.instructions": "The name on your account and the name on your ID must be an exact match. If not, please click \"No\" to update your account name.",
"id.verification.account.name.radio.label": "Does the name on your ID match the Account Name below?",
"id.verification.account.name.radio.yes": "Yes",
"id.verification.account.name.radio.no": "No",
"id.verification.account.name.error": "Please update account name to match the name on your ID.",
"id.verification.account.name.warning.prefix": "Please Note:",
"id.verification.account.name.settings": "Account Settings",
"id.verification.account.name.label": "Name on ID",
"id.verification.account.name.edit": "Edit",
"id.verification.account.name.label": "Account Name",
"id.verification.account.name.photo.alt": "Photo of your ID to be submitted.",
"id.verification.account.name.save": "Save",
"id.verification.account.name.save": "Save and Next",
"id.verification.review.title": "Review Your Photos",
"id.verification.review.description": "Make sure we can verify your identity with the photos and information you have provided.",
"id.verification.review.portrait.label": "Your Portrait",
@@ -286,11 +279,14 @@
"id.verification.review.id.label": "Your Photo ID",
"id.verification.review.id.alt": "Photo of your ID to be submitted.",
"id.verification.review.id.retake": "Retake ID Photo",
"id.verification.review.confirm": "Confirm",
"id.verification.review.confirm": "Submit",
"id.verification.review.error": "edX Support Page",
"id.verification.submitted.title": "Identity Verification in Progress",
"id.verification.submitted.text": "We have received your information and are verifying your identity. You will see a message on your dashboard when the verification process is complete (usually within 1-2 days). In the meantime, you can still access all available course content.",
"id.verification.submitted.return": "Return to Your Dashboard",
"id.verification.account.name.warning": "{prefix} Any edit to your name will be saved to your account and can be reviewed on {accountSettings}.",
"id.verification.submitted.text": "We have received your information and are verifying your identity. You will see a message on your dashboard when the verification process is complete (usually within 5 days). In the meantime, you can still access all available course content.",
"id.verification.return.dashboard": "Return to Your Dashboard",
"id.verification.return.course": "Return to Course",
"id.verification.request.camera.access.instructions": "In order to take a photo using your webcam, you may receive a browser prompt for access to your camera. {clickAllow}",
"id.verification.requirements.card.device.text": "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}."
"id.verification.requirements.card.device.text": "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}.",
"idv.submission.alert.error": "\n We encountered a technical error while trying to submit ID verification.\n This might be a temporary issue, so please try again in a few minutes.\n If the problem persists,\n please go to {support_link} for help.\n ",
"id.verification.account.name.edit": "Edit{sr}"
}

View File

@@ -1,7 +1,10 @@
import React from 'react';
import PropTypes from 'prop-types';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import * as blazeface from '@tensorflow-models/blazeface';
import CameraPhoto, { FACING_MODES } from 'jslib-html5-camera-photo';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Form, Spinner } from '@edx/paragon';
import shutter from './data/camera-shutter.base64.json';
import messages from './IdVerification.messages';
@@ -11,23 +14,220 @@ class Camera extends React.Component {
super(props, context);
this.cameraPhoto = null;
this.videoRef = React.createRef();
this.canvasRef = React.createRef();
this.setDetection = this.setDetection.bind(this);
this.state = {
trackedObject: null,
dataUri: '',
videoHasLoaded: false,
shouldDetect: false,
isFinishedLoadingDetection: true,
shouldGiveFeedback: true,
feedback: '',
};
}
componentDidMount() {
this.cameraPhoto = new CameraPhoto(this.videoRef.current);
this.cameraPhoto.startCameraMaxResolution(FACING_MODES.USER);
this.cameraPhoto.startCamera(
this.props.isPortrait ? FACING_MODES.USER : FACING_MODES.ENVIRONMENT,
{ width: 640, height: 480 },
);
}
async componentWillUnmount() {
this.cameraPhoto.stopCamera();
}
sendEvent() {
let eventName = 'edx.id_verification';
if (this.props.isPortrait) {
eventName += '.user_photo';
} else {
eventName += '.id_photo';
}
if (this.state.shouldDetect) {
eventName += '.face_detection_enabled';
} else {
eventName += '.face_detection_disabled';
}
sendTrackEvent(eventName);
}
setDetection() {
this.setState(
{ shouldDetect: !this.state.shouldDetect },
() => {
if (this.state.shouldDetect) {
this.setState({ isFinishedLoadingDetection: false });
this.startDetection();
}
this.sendEvent();
},
);
}
startDetection() {
setTimeout(() => {
if (this.state.videoHasLoaded) {
const loadModelPromise = blazeface.load();
Promise.all([loadModelPromise])
.then((values) => {
this.setState({ isFinishedLoadingDetection: true });
this.detectFromVideoFrame(values[0], this.videoRef.current);
});
} else {
this.setState({ isFinishedLoadingDetection: true });
this.setState({ shouldDetect: false });
// TODO: add error message
}
}, 1000);
}
detectFromVideoFrame = (model, video) => {
model.estimateFaces(video).then((predictions) => {
if (this.state.shouldDetect && !this.state.dataUri) {
this.showDetections(predictions);
requestAnimationFrame(() => {
this.detectFromVideoFrame(model, video);
});
}
});
};
showDetections = (predictions) => {
let canvasContext;
if (predictions.length > 0) {
canvasContext = this.canvasRef.current.getContext('2d');
canvasContext.clearRect(0, 0, canvasContext.canvas.width, canvasContext.canvas.height);
}
// predictions is an array of objects describing each detected face
predictions.forEach((prediction) => {
const start = [prediction.topLeft[0], prediction.topLeft[1]];
const end = [prediction.bottomRight[0], prediction.bottomRight[1]];
const size = [end[0] - start[0], end[1] - start[1]];
// landmarks is an array of points representing each facial landmark (i.e. right eye, left eye, nose, etc.)
const features = prediction.landmarks;
let isInPosition = true;
// for each of the landmarks, determine if it is in position
for (let j = 0; j < features.length; j++) {
const x = features[j][0];
const y = features[j][1];
let isInRange;
if (this.props.isPortrait) {
isInRange = this.isInRangeForPortrait(x, y);
} else {
isInRange = this.isInRangeForID(x, y);
}
// if it is not in range, give feedback depending on which feature is out of range
isInPosition = isInPosition && isInRange;
}
// draw a box depending on if all landmarks are in position
if (isInPosition) {
canvasContext.strokeStyle = '#00ffff';
canvasContext.lineWidth = 6;
canvasContext.strokeRect(start[0], start[1], size[0], size[1]);
// give positive feedback here if user is in correct position
this.giveFeedback(predictions.length, [], true);
} else {
canvasContext.fillStyle = 'rgba(255, 51, 0, 0.75)';
canvasContext.fillRect(start[0], start[1], size[0], size[1]);
this.giveFeedback(predictions.length, features[0], false);
}
});
if (predictions.length === 0) {
this.giveFeedback(predictions.length, [], false);
}
}
giveFeedback(numFaces, rightEye, isCorrect) {
if (this.state.shouldGiveFeedback) {
const currentFeedback = this.state.feedback;
let newFeedback = '';
if (numFaces === 1) {
// only give feedback if one face is detected otherwise
// it would be difficult to tell a user which face to move
if (isCorrect) {
newFeedback = this.props.intl.formatMessage(messages['id.verification.photo.feedback.correct']);
} else {
// give feedback based on where user is
newFeedback = this.props.intl.formatMessage(messages[this.getGridPosition(rightEye)]);
}
} else if (numFaces > 1) {
newFeedback = this.props.intl.formatMessage(messages['id.verification.photo.feedback.two.faces']);
} else {
newFeedback = this.props.intl.formatMessage(messages['id.verification.photo.feedback.no.faces']);
}
if (currentFeedback !== newFeedback) {
// only update status if it is different, so we don't overload the user with status updates
this.setState({ feedback: newFeedback });
}
// turn off feedback for one to ensure that instructions aren't disruptive/interrupting
this.setState({ shouldGiveFeedback: false });
setTimeout(() => {
this.setState({ shouldGiveFeedback: true });
}, 1000);
}
}
getGridPosition(coordinates) {
// Used to determine where a face is (i.e. top-left, center-right, bottom-center, etc.)
const x = coordinates[0];
const y = coordinates[1];
let messageBase = 'id.verification.photo.feedback';
const heightUpperLimit = 320;
const heightMiddleLimit = 160;
if (y < heightMiddleLimit) {
messageBase += '.top';
} else if (y < heightUpperLimit && y >= heightMiddleLimit) {
messageBase += '.center';
} else {
messageBase += '.bottom';
}
const widthRightLimit = 213;
const widthMiddleLimit = 427;
if (x < widthRightLimit) {
messageBase += '.right';
} else if (x >= widthRightLimit && x < widthMiddleLimit) {
messageBase += '.center';
} else {
messageBase += '.left';
}
return messageBase;
}
isInRangeForPortrait(x, y) {
return x > 47 && x < 570 && y > 100 && y < 410;
}
isInRangeForID(x, y) {
return x > 120 && x < 470 && y > 120 && y < 350;
}
setVideoHasLoaded() {
this.setState({ videoHasLoaded: 'true' });
}
takePhoto() {
if (this.state.dataUri) {
return this.reset();
}
const config = {
sizeFactor: 1,
sizeFactor: this.getSizeFactor(),
};
this.playShutterClick();
@@ -36,13 +236,40 @@ class Camera extends React.Component {
this.props.onImageCapture(dataUri);
}
getSizeFactor() {
let sizeFactor = 1;
const settings = this.cameraPhoto.getCameraSettings();
if (settings) {
const videoWidth = settings.width;
const videoHeight = settings.height;
// need to multiply by 3 because each pixel contains 3 bytes
const currentSize = videoWidth * videoHeight * 3;
// chose a limit of 9,999,999 (bytes) so that result will
// always be less than 10MB
const ratio = 9999999 / currentSize;
if (ratio < 1) {
// if the current resolution creates an image larger than 10 MB, adjust sizeFactor (resolution)
// to ensure that image will have a file size of less than 10 MB.
sizeFactor = ratio;
} else if (videoWidth === 640 && videoHeight === 480) {
// otherwise increase the resolution to try and prevent blurry images.
sizeFactor = 2;
}
}
return sizeFactor;
}
playShutterClick() {
const audio = new Audio('data:audio/mp3;base64,' + shutter.base64);
const audio = new Audio(`data:audio/mp3;base64,${shutter.base64}`);
audio.play();
}
reset() {
this.setState({ dataUri: '' });
if (this.state.shouldDetect) {
this.startDetection();
}
}
render() {
@@ -50,25 +277,67 @@ class Camera extends React.Component {
? 'do-transition camera-flash'
: 'camera-flash';
return (
<div className='camera-outer-wrapper shadow'>
<div className='camera-wrapper'>
<div className="camera-outer-wrapper shadow">
<Form.Group style={{ textAlign: 'left', padding: '0.5rem', marginBottom: '0.5rem' }} >
<Form.Check
id="videoDetection"
name="videoDetection"
label={this.props.intl.formatMessage(messages['id.verification.photo.enable.detection'])}
aria-describedby="videoDetectionHelpText"
checked={this.state.shouldDetect}
onChange={this.setDetection}
style={{ padding: '0rem', marginLeft: '1.25rem', float: this.state.isFinishedLoadingDetection ? 'none' : 'left' }}
/>
{!this.state.isFinishedLoadingDetection && <Spinner animation="border" variant="primary" style={{ marginLeft: '0.5rem' }} data-testid="spinner" />}
<Form.Text id="videoDetectionHelpText" data-testid="videoDetectionHelpText">
{this.props.isPortrait
? this.props.intl.formatMessage(messages['id.verification.photo.enable.detection.portrait.help.text'])
: this.props.intl.formatMessage(messages['id.verification.photo.enable.detection.id.help.text'])}
</Form.Text>
</Form.Group>
<div className="camera-wrapper">
<div className={cameraFlashClass} />
<video
ref={this.videoRef}
autoPlay={true}
className='camera-video'
style={{ display: this.state.dataUri ? 'none' : 'block' }}
data-testid="video"
autoPlay
className="camera-video"
onLoadedData={() => { this.setVideoHasLoaded(); }}
style={{
display: this.state.dataUri ? 'none' : 'block',
WebkitTransform: 'scaleX(-1)',
transform: 'scaleX(-1)',
}}
playsInline
/>
<canvas
ref={this.canvasRef}
data-testid="detection-canvas"
className="canvas-video"
style={{
display: !this.state.shouldDetect || this.state.dataUri ? 'none' : 'block',
WebkitTransform: 'scaleX(-1)',
transform: 'scaleX(-1)',
}}
width="640"
height="480"
/>
<img
alt='imgCamera'
data-hj-suppress
alt="imgCamera"
src={this.state.dataUri}
className='camera-video'
className="camera-video"
style={{ display: this.state.dataUri ? 'block' : 'none' }}
/>
<div role="status" className="sr-only">{this.state.feedback}</div>
</div>
<button
className='btn btn-primary camera-btn'
accessKey='c'
className={`btn camera-btn ${
this.state.dataUri ?
'btn-outline-primary'
: 'btn-primary'
}`}
accessKey="c"
onClick={() => {
this.takePhoto();
}}
@@ -85,6 +354,7 @@ class Camera extends React.Component {
Camera.propTypes = {
intl: intlShape.isRequired,
onImageCapture: PropTypes.func.isRequired,
isPortrait: PropTypes.bool.isRequired,
};
export default injectIntl(Camera);

View File

@@ -1,5 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Collapsible } from '@edx/paragon';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
@@ -12,18 +13,20 @@ function CameraHelp(props) {
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.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.head.question'])}
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.head.answer'])}
{props.intl.formatMessage(messages['id.verification.camera.help.difficulty.answer'])}
</p>
</Collapsible>
</div>
@@ -32,6 +35,13 @@ function CameraHelp(props) {
CameraHelp.propTypes = {
intl: intlShape.isRequired,
isOpen: PropTypes.bool,
isPortrait: PropTypes.bool,
};
CameraHelp.defaultProps = {
isOpen: false,
isPortrait: false,
};
export default injectIntl(CameraHelp);

View File

@@ -0,0 +1,53 @@
import React, { useState, useContext } from 'react';
import PropTypes from 'prop-types';
import { Collapsible } from '@edx/paragon';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import messages from './IdVerification.messages';
import ImageFileUpload from './ImageFileUpload';
import { IdVerificationContext } from './IdVerificationContext';
import ImagePreview from './ImagePreview';
function CameraHelpWithUpload(props) {
const { setIdPhotoFile, idPhotoFile, userId } = useContext(IdVerificationContext);
const [hasUploadedImage, setHasUploadedImage] = useState(false);
function setAndTrackIdPhotoFile(image) {
sendTrackEvent('edx.id_verification.upload_id', {
category: 'id_verification',
user_id: userId,
});
setHasUploadedImage(true);
setIdPhotoFile(image);
}
return (
<div>
<Collapsible
styling="card"
title={props.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'])} />}
<p>
{props.intl.formatMessage(messages['id.verification.id.photo.instructions.upload'])}
</p>
<ImageFileUpload onFileChange={setAndTrackIdPhotoFile} intl={props.intl} />
</Collapsible>
</div>
);
}
CameraHelpWithUpload.propTypes = {
intl: intlShape.isRequired,
isOpen: PropTypes.bool,
};
CameraHelpWithUpload.defaultProps = {
isOpen: false,
};
export default injectIntl(CameraHelpWithUpload);

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
import messages from './IdVerification.messages';
@@ -10,13 +10,22 @@ function ExistingRequest(props) {
<h3 aria-level="1" tabIndex="-1">
{props.intl.formatMessage(messages['id.verification.existing.request.title'])}
</h3>
{props.status === 'pending' || props.status == 'approved'
{props.status === 'pending' || props.status === 'approved'
? <p>{props.intl.formatMessage(messages['id.verification.existing.request.pending.text'])}</p>
: <p>{props.intl.formatMessage(messages['id.verification.existing.request.denied.text'])}</p>
: <FormattedMessage
id="id.verification.existing.request.denied.text"
defaultMessage="You cannot verify your identity at this time. If you have yet to activate your account, please check your spam folder for the activation email from {email}."
description="Text that displays when user is denied from making a request, and to check their email for an activation email."
values={{
email: <strong>no-reply@registration.edx.org</strong>,
}}
/>
}
<a className="btn btn-primary" href={`${getConfig().LMS_BASE_URL}/dashboard`}>
{props.intl.formatMessage(messages['id.verification.return'])}
</a>
<div className="action-row">
<a className="btn btn-primary mt-3" href={`${getConfig().LMS_BASE_URL}/dashboard`}>
{props.intl.formatMessage(messages['id.verification.return.dashboard'])}
</a>
</div>
</div>
);
}

View File

@@ -68,14 +68,9 @@ const messages = defineMessages({
},
'id.verification.existing.request.pending.text': {
id: 'id.verification.existing.request.pending.text',
defaultMessage: 'You have already submitted your verification information. You will see a message on your dashboard when the verification process is complete (usually within 1-2 days).',
defaultMessage: 'You have already submitted your verification information. You will see a message on your dashboard when the verification process is complete (usually within 5 days).',
description: 'Text that displays when user has a pending or approved request.',
},
'id.verification.existing.request.denied.text': {
id: 'id.verification.existing.request.denied.text',
defaultMessage: 'You cannot verify your identity at this time.',
description: 'Text that displays when user is denied from making a request.',
},
'id.verification.photo.take': {
id: 'id.verification.photo.take',
defaultMessage: 'Take Photo',
@@ -86,11 +81,96 @@ const messages = defineMessages({
defaultMessage: 'Retake Photo',
description: 'Button to retake photo.',
},
'id.verification.photo.enable.detection': {
id: 'id.verification.photo.enable.detection',
defaultMessage: 'Enable Face Detection',
description: 'Text label for the checkbox to enable face detection.',
},
'id.verification.photo.enable.detection.portrait.help.text': {
id: 'id.verification.photo.enable.detection.portrait.help.text',
defaultMessage: 'If checked, a box will appear around your face. Your face can be seen clearly if the box around it is blue. If your face is not in a good position or undetectable, the box will be red.',
description: 'Help text that appears for enabling face detection on the portrait photo panel.',
},
'id.verification.photo.enable.detection.id.help.text': {
id: 'id.verification.photo.enable.detection.id.help.text',
defaultMessage: 'If checked, a box will appear around the face on your ID card. The face can be seen clearly if the box around it is blue. If the face is not in a good position or undetectable, the box will be red.',
description: 'Help text that appears for enabling face detection on the portrait photo panel.',
},
'id.verification.photo.feedback.correct': {
id: 'id.verification.photo.feedback.correct',
defaultMessage: 'Face is in a good position.',
description: 'Text for screen reader when user\'s face is in a good position.',
},
'id.verification.photo.feedback.two.faces': {
id: 'id.verification.photo.feedback.two.faces',
defaultMessage: 'More than one face detected.',
description: 'Text for screen reader when more than one face detected.',
},
'id.verification.photo.feedback.no.faces': {
id: 'id.verification.photo.feedback.no.faces',
defaultMessage: 'No face detected.',
description: 'Text for screen reader when no face detected.',
},
'id.verification.photo.feedback.top.left': {
id: 'id.verification.photo.feedback.top.left',
defaultMessage: 'Incorrect position. Top left.',
description: 'Text for screen reader when face is in a bad position.',
},
'id.verification.photo.feedback.top.center': {
id: 'id.verification.photo.feedback.top.center',
defaultMessage: 'Incorrect position. Top center.',
description: 'Text for screen reader when face is in a bad position.',
},
'id.verification.photo.feedback.top.right': {
id: 'id.verification.photo.feedback.top.right',
defaultMessage: 'Incorrect position. Top right.',
description: 'Text for screen reader when face is in a bad position.',
},
'id.verification.photo.feedback.center.left': {
id: 'id.verification.photo.feedback.center.left',
defaultMessage: 'Incorrect position. Center left.',
description: 'Text for screen reader when face is in a bad position.',
},
'id.verification.photo.feedback.center.center': {
id: 'id.verification.photo.feedback.center.center',
defaultMessage: 'Incorrect position. Too close to camera.',
description: 'Text for screen reader when face is in a bad position.',
},
'id.verification.photo.feedback.center.right': {
id: 'id.verification.photo.feedback.center.right',
defaultMessage: 'Incorrect position. Center right.',
description: 'Text for screen reader when face is in a bad position.',
},
'id.verification.photo.feedback.bottom.left': {
id: 'id.verification.photo.feedback.bottom.left',
defaultMessage: 'Incorrect position. Bottom left.',
description: 'Text for screen reader when face is in a bad position.',
},
'id.verification.photo.feedback.bottom.center': {
id: 'id.verification.photo.feedback.bottom.center',
defaultMessage: 'Incorrect position. Bottom center.',
description: 'Text for screen reader when face is in a bad position.',
},
'id.verification.photo.feedback.bottom.right': {
id: 'id.verification.photo.feedback.bottom.right',
defaultMessage: 'Incorrect position. Bottom right.',
description: 'Text for screen reader when face is in a bad position.',
},
'id.verification.camera.access.title': {
id: 'id.verification.camera.access.title',
defaultMessage: 'Camera Permissions',
description: 'Title for the Camera Access page.',
},
'id.verification.camera.access.title.success': {
id: 'id.verification.camera.access.title.success',
defaultMessage: 'Camera Access Enabled',
description: 'Title for the Camera Access page when camera is enabled.',
},
'id.verification.camera.access.title.failed': {
id: 'id.verification.camera.access.title.failed',
defaultMessage: 'Camera Access Failed',
description: 'Title for the Camera Access page when camera access is denied or unavailable.',
},
'id.verification.camera.access.click.allow': {
id: 'id.verification.camera.access.click.allow',
defaultMessage: 'Please make sure to click "Allow"',
@@ -121,6 +201,136 @@ const messages = defineMessages({
defaultMessage: 'It looks like we\'re unable to access your camera. You will need to upload image files of you and your photo id.',
description: 'Text indicating that the camera could not be accessed and image upload will be enabled.',
},
'id.verification.camera.access.failure.temporary': {
id: 'id.verification.camera.access.failure.temporary',
defaultMessage: 'It looks like we\'re unable to access your camera. Please verify that your webcam is connected and that you have allowed your browser to access it.',
description: 'Text indicating that the camera could not be accessed.',
},
'id.verification.camera.access.failure.temporary.chrome': {
id: 'id.verification.camera.access.failure.temporary.chrome',
defaultMessage: 'To enable camera access in Chrome:',
description: 'Description for the directions on enabling camera access.',
},
'id.verification.camera.access.failure.temporary.chrome.step1': {
id: 'id.verification.camera.access.failure.temporary.chrome.step1',
defaultMessage: 'Open Chrome.',
description: 'Text for step one of enabling camera access.',
},
'id.verification.camera.access.failure.temporary.chrome.step2': {
id: 'id.verification.camera.access.failure.temporary.chrome.step2',
defaultMessage: 'Navigate to More > Settings.',
description: 'Text for step two of enabling camera access.',
},
'id.verification.camera.access.failure.temporary.chrome.step2.windows': {
id: 'id.verification.camera.access.failure.temporary.chrome.step2.windows',
defaultMessage: 'For Windows: Alt+F, Alt+E, or F10 followed by the spacebar',
description: 'Text for Windows keyboard shortcut in chrome.',
},
'id.verification.camera.access.failure.temporary.chrome.step2.mac': {
id: 'id.verification.camera.access.failure.temporary.chrome.step2.mac',
defaultMessage: 'For Mac: Command+,',
description: 'Text for Mac keyboard shortcut in chrome.',
},
'id.verification.camera.access.failure.temporary.chrome.step3': {
id: 'id.verification.camera.access.failure.temporary.chrome.step3',
defaultMessage: 'Under the "Privacy and security" tab, select "Site Settings" and then "Camera."',
description: 'Text for step three of enabling camera access.',
},
'id.verification.camera.access.failure.temporary.chrome.step4': {
id: 'id.verification.camera.access.failure.temporary.chrome.step4',
defaultMessage: 'Under "Blocked," find "edx.org" and select it.',
description: 'Text for step four of enabling camera access.',
},
'id.verification.camera.access.failure.temporary.chrome.step5': {
id: 'id.verification.camera.access.failure.temporary.chrome.step5',
defaultMessage: 'In the "Permissions" section, update the camera permissions to "Allow."',
description: 'Text for step five of enabling camera access.',
},
'id.verification.camera.access.failure.temporary.ie11': {
id: 'id.verification.camera.access.failure.temporary.ie11',
defaultMessage: 'To enable camera access in Internet Explorer:',
description: 'Description for the directions on enabling camera access.',
},
'id.verification.camera.access.failure.temporary.ie11.step1': {
id: 'id.verification.camera.access.failure.temporary.ie11.step1',
defaultMessage: 'Open the Flash Player Settings Manager by navigating to Windows Settings > Control Panel > Flash Player.',
description: 'Text for step one of enabling camera access.',
},
'id.verification.camera.access.failure.temporary.ie11.step2': {
id: 'id.verification.camera.access.failure.temporary.ie11.step2',
defaultMessage: 'Select the "Camera and Mic" tab, and then select the "Camera and Microphone Settings by Site" button.',
description: 'Text for step two of enabling camera access.',
},
'id.verification.camera.access.failure.temporary.ie11.step3': {
id: 'id.verification.camera.access.failure.temporary.ie11.step3',
defaultMessage: 'Choose "edx.org" from the list of websites and change the permissions by selecting "Allow" in the dropdown menu.',
description: 'Text for step three of enabling camera access.',
},
'id.verification.camera.access.failure.temporary.firefox': {
id: 'id.verification.camera.access.failure.temporary.firefox',
defaultMessage: 'To enable camera access in Firefox:',
description: 'Description for the directions on enabling camera access.',
},
'id.verification.camera.access.failure.temporary.firefox.step1': {
id: 'id.verification.camera.access.failure.temporary.firefox.step1',
defaultMessage: 'Open Firefox.',
description: 'Text for step one of enabling camera access.',
},
'id.verification.camera.access.failure.temporary.firefox.step2': {
id: 'id.verification.camera.access.failure.temporary.firefox.step2',
defaultMessage: 'Enter "about:preferences" in the URL bar.',
description: 'Text for step two of enabling camera access.',
},
'id.verification.camera.access.failure.temporary.firefox.step3': {
id: 'id.verification.camera.access.failure.temporary.firefox.step3',
defaultMessage: 'Select the "Privacy & Security" tab, and navigate to the "Permissions" section.',
description: 'Text for step three of enabling camera access.',
},
'id.verification.camera.access.failure.temporary.firefox.step4': {
id: 'id.verification.camera.access.failure.temporary.firefox.step4',
defaultMessage: 'Next to "Camera," select the "Settings…" button.',
description: 'Text for step four of enabling camera access.',
},
'id.verification.camera.access.failure.temporary.firefox.step5': {
id: 'id.verification.camera.access.failure.temporary.firefox.step5',
defaultMessage: 'In the search bar, enter "edx.org."',
description: 'Text for step five of enabling camera access.',
},
'id.verification.camera.access.failure.temporary.firefox.step6': {
id: 'id.verification.camera.access.failure.temporary.firefox.step6',
defaultMessage: 'In the status column for "edx.org," select "Allow" from the drop down.',
description: 'Text for step six of enabling camera access.',
},
'id.verification.camera.access.failure.temporary.firefox.step7': {
id: 'id.verification.camera.access.failure.temporary.firefox.step7',
defaultMessage: 'Select "Save Changes."',
description: 'Text for step seven of enabling camera access.',
},
'id.verification.camera.access.failure.temporary.safari': {
id: 'id.verification.camera.access.failure.temporary.safari',
defaultMessage: 'To enable camera access in Safari:',
description: 'Description for the directions on enabling camera access.',
},
'id.verification.camera.access.failure.temporary.safari.step1': {
id: 'id.verification.camera.access.failure.temporary.safari.step1',
defaultMessage: 'Open Safari.',
description: 'Text for step one of enabling camera access.',
},
'id.verification.camera.access.failure.temporary.safari.step2': {
id: 'id.verification.camera.access.failure.temporary.safari.step2',
defaultMessage: 'Click on the Safari app menu, then select "Preferences." You can also use Command+, as a keyboard shortcut.',
description: 'Text for step two of enabling camera access.',
},
'id.verification.camera.access.failure.temporary.safari.step3': {
id: 'id.verification.camera.access.failure.temporary.safari.step3',
defaultMessage: 'Select the "Websites" tab and then select "Camera."',
description: 'Text for step three of enabling camera access.',
},
'id.verification.camera.access.failure.temporary.safari.step4': {
id: 'id.verification.camera.access.failure.temporary.safari.step4',
defaultMessage: 'Select "edx.org" and change the camera permissions to "Allow."',
description: 'Text for step four of enabling camera access.',
},
'id.verification.photo.tips.title': {
id: 'id.verification.photo.tips.title',
defaultMessage: 'Helpful Photo Tips',
@@ -153,12 +363,12 @@ const messages = defineMessages({
},
'id.verification.portrait.photo.title.camera': {
id: 'id.verification.portrait.photo.title.camera',
defaultMessage: 'Take Your Photo',
defaultMessage: 'Take a Photo of Yourself',
description: 'Title for the Portrait Photo page if camera access is enabled.',
},
'id.verification.portrait.photo.title.upload': {
id: 'id.verification.portrait.photo.title.upload',
defaultMessage: 'Upload Your Portrait Photo',
defaultMessage: 'Upload a Photo of Yourself',
description: 'Title for the Portrait Photo page if camera access is disabled.',
},
'id.verification.portrait.photo.preview.alt': {
@@ -181,21 +391,36 @@ const messages = defineMessages({
defaultMessage: 'What if I can\'t see the camera image or if I can\'t see my photo to determine which side is visible?',
description: 'Question on what to do if the user cannot see the camera image or photo during verification.',
},
'id.verification.camera.help.sight.answer': {
id: 'id.verification.camera.help.sight.answer',
defaultMessage: 'You may be able to complete the image capture procedure without assistance, but it may take a couple of submission attempts to get the camera positioning right. Optimal camera positioning varies with each computer, but generally the best position for a headshot is approximately 12-18 inches (30-45 centimeters) from the camera, with your head centered relative to the computer screen. If the photos you submit are rejected, try moving the computer or camera orientation to change the lighting angle. The most common reason for rejection is inability to read the text on the ID card.',
description: 'Confirming what to do if the camera image or photo cannot be seen during verification.',
'id.verification.camera.help.sight.answer.portrait': {
id: 'id.verification.camera.help.sight.answer.portrait',
defaultMessage: 'You may be able to complete the image capture procedure without assistance, but it may take a couple of submission attempts to get the camera positioning right. Optimal camera positioning varies with each computer, but generally the best position for a headshot is approximately 12-18 inches (30-45 centimeters) from the camera, with your head centered relative to the computer screen. If the photos you submit are rejected, try moving the computer or camera orientation to change the lighting angle.',
description: 'Confirming what to do if the camera image of the portrait cannot be seen during verification.',
},
'id.verification.camera.help.head.question': {
id: 'id.verification.camera.help.head.question',
'id.verification.camera.help.sight.answer.id': {
id: 'id.verification.camera.help.sight.answer.id',
defaultMessage: 'You may be able to complete the image capture procedure without assistance, but it may take a couple of submission attempts to get the camera positioning right. Optimal camera positioning varies with each computer, but generally, the best position for a photo of an ID card is 8-12 inches (20-30 centimeters) from the camera, with the ID card centered relative to the camera. If the photos you submit are rejected, try moving the computer or camera orientation to change the lighting angle. The most common reason for rejection is inability to read the text on the ID card.',
description: 'Confirming what to do if the camera image of the ID cannot be seen during verification.',
},
'id.verification.camera.help.difficulty.question.portrait': {
id: 'id.verification.camera.help.difficulty.question.portrait',
defaultMessage: 'What if I have difficulty holding my head in position relative to the camera?',
description: 'Question on what to do if the user has difficulty holding their head relative to the camera.',
},
'id.verification.camera.help.head.answer': {
id: 'id.verification.camera.help.head.answer',
'id.verification.camera.help.difficulty.question.id': {
id: 'id.verification.camera.help.difficulty.question.id',
defaultMessage: 'What if I have difficulty holding my ID in position relative to the camera?',
description: 'Question on what to do if the user has difficulty holding their ID relative to the camera.',
},
'id.verification.camera.help.difficulty.answer': {
id: 'id.verification.camera.help.difficulty.answer',
defaultMessage: 'If you require assistance with taking a photo for submission, contact edX support for additional suggestions.',
description: 'Confirming what to do if the user has difficult holding their head relative to the camera.',
},
'id.verification.id.photo.unclear.question': {
id: 'id.verification.id.photo.unclear.question',
defaultMessage: 'Is your ID image not clear or too blurry?',
description: 'Question on what to do if the user\'s ID image is unclear',
},
'id.verification.id.tips.title': {
id: 'id.verification.id.tips.title',
defaultMessage: 'Helpful ID Tips',
@@ -203,7 +428,7 @@ const messages = defineMessages({
},
'id.verification.id.tips.description': {
id: 'id.verification.id.tips.description',
defaultMessage: 'Next you\'ll need an eligible ID photo, make sure that:',
defaultMessage: 'Next, we\'ll need you to take a photo of a valid ID that includes your name. Please have your ID ready.',
description: 'Description for the ID Tips page.',
},
'id.verification.id.tips.list.well.lit': {
@@ -218,12 +443,12 @@ const messages = defineMessages({
},
'id.verification.id.photo.title.camera': {
id: 'id.verification.id.photo.title.camera',
defaultMessage: 'Take ID Photo',
defaultMessage: 'Take a Photo of Your ID',
description: 'Title for the ID Photo page if camera access is enabled.',
},
'id.verification.id.photo.title.upload': {
id: 'id.verification.id.photo.title.upload',
defaultMessage: 'Upload Your ID Photo',
defaultMessage: 'Upload a Photo of Your ID',
description: 'Title for the ID Photo page if camera access is disabled.',
},
'id.verification.id.photo.preview.alt': {
@@ -238,9 +463,14 @@ const messages = defineMessages({
},
'id.verification.id.photo.instructions.upload': {
id: 'id.verification.id.photo.instructions.upload',
defaultMessage: 'Please upload an ID photo. Ensure the entire ID fits inside the frame and is well-lit. (Supported formats: .jpg, .jpeg, .png)',
defaultMessage: 'Please upload an ID photo. Ensure the entire ID fits inside the frame and is well-lit. The file size must be under 10 MB. (Supported formats: .jpg, .jpeg, .png)',
description: 'Instructions for ID photo upload.',
},
'id.verification.id.photo.instructions.upload.error': {
id: 'id.verification.id.photo.instructions.upload.error',
defaultMessage: 'The file you have selected is too large. Please try again with a file less than 10MB.',
description: 'Error message for file upload that is larger than 10MB.',
},
'id.verification.account.name.title': {
id: 'id.verification.account.name.title',
defaultMessage: 'Account Name Check',
@@ -248,9 +478,29 @@ const messages = defineMessages({
},
'id.verification.account.name.instructions': {
id: 'id.verification.account.name.instructions',
defaultMessage: 'Please check the Account Name below to ensure it matches the name on your ID. If not, click "Edit".',
defaultMessage: 'The name on your account and the name on your ID must be an exact match. If not, please click "No" to update your account name.',
description: 'Text to verify that the account name matches the name on the ID photo.',
},
'id.verification.account.name.radio.label': {
id: 'id.verification.account.name.radio.label',
defaultMessage: 'Does the name on your ID match the Account Name below?',
description: 'Question to ask the user whether their account name match the name on their ID card.',
},
'id.verification.account.name.radio.yes': {
id: 'id.verification.account.name.radio.yes',
defaultMessage: 'Yes',
description: 'The radio button that says the account name matches.',
},
'id.verification.account.name.radio.no': {
id: 'id.verification.account.name.radio.no',
defaultMessage: 'No',
description: 'The radio button that says the account name does not match.',
},
'id.verification.account.name.error': {
id: 'id.verification.account.name.error',
defaultMessage: 'Please update account name to match the name on your ID.',
description: 'Error that shows when the user needs to update their account name to match the name on their ID.',
},
'id.verification.account.name.warning.prefix': {
id: 'id.verification.account.name.warning.prefix',
defaultMessage: 'Please Note:',
@@ -263,14 +513,9 @@ const messages = defineMessages({
},
'id.verification.account.name.label': {
id: 'id.verification.account.name.label',
defaultMessage: 'Name on ID',
defaultMessage: 'Account Name',
description: 'Label for account name input.',
},
'id.verification.account.name.edit': {
id: 'id.verification.account.name.edit',
defaultMessage: 'Edit',
description: 'Button to edit account name.',
},
'id.verification.account.name.photo.alt': {
id: 'id.verification.account.name.photo.alt',
defaultMessage: 'Photo of your ID to be submitted.',
@@ -278,7 +523,7 @@ const messages = defineMessages({
},
'id.verification.account.name.save': {
id: 'id.verification.account.name.save',
defaultMessage: 'Save',
defaultMessage: 'Save and Next',
description: 'Button to save the account name.',
},
'id.verification.review.title': {
@@ -323,8 +568,13 @@ const messages = defineMessages({
},
'id.verification.review.confirm': {
id: 'id.verification.review.confirm',
defaultMessage: 'Confirm',
description: 'Button to confirm all information is correct.',
defaultMessage: 'Submit',
description: 'Button to confirm all information is correct and submit.',
},
'id.verification.review.error': {
id: 'id.verification.review.error',
defaultMessage: 'edX Support Page',
description: 'Text linking to the support page.',
},
'id.verification.submitted.title': {
id: 'id.verification.submitted.title',
@@ -333,14 +583,19 @@ const messages = defineMessages({
},
'id.verification.submitted.text': {
id: 'id.verification.submitted.text',
defaultMessage: 'We have received your information and are verifying your identity. You will see a message on your dashboard when the verification process is complete (usually within 1-2 days). In the meantime, you can still access all available course content.',
defaultMessage: 'We have received your information and are verifying your identity. You will see a message on your dashboard when the verification process is complete (usually within 5 days). In the meantime, you can still access all available course content.',
description: 'Text confirming that ID verification request has been received.',
},
'id.verification.return': {
id: 'id.verification.submitted.return',
'id.verification.return.dashboard': {
id: 'id.verification.return.dashboard',
defaultMessage: 'Return to Your Dashboard',
description: 'Button to return to the dashboard.',
},
'id.verification.return.course': {
id: 'id.verification.return.course',
defaultMessage: 'Return to Course',
description: 'Return to the course which ID verification was accessed from.',
},
});
export default messages;

View File

@@ -34,6 +34,7 @@ function IdVerificationContextProvider({ children }) {
idPhotoName,
mediaStream,
mediaAccess,
userId: authenticatedUser.userId,
nameOnAccount: authenticatedUser.name,
setExistingIdVerification,
setFacePhotoFile,
@@ -44,13 +45,20 @@ function IdVerificationContextProvider({ children }) {
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
setMediaAccess(MEDIA_ACCESS.GRANTED);
setMediaStream(stream);
// If we would like to stop the stream immediately. I guess we can leave it open
// const tracks = stream.getTracks();
// tracks.forEach(track => track.stop());
// stop the stream, as we are not using it yet
const tracks = stream.getTracks();
tracks.forEach(track => track.stop());
} catch (err) {
setMediaAccess(MEDIA_ACCESS.DENIED);
}
},
stopUserMedia: () => {
if (mediaStream) {
const tracks = mediaStream.getTracks();
tracks.forEach(track => track.stop());
setMediaStream(null);
}
},
};
// Call verification status endpoint to check whether we can verify.

View File

@@ -1,6 +1,6 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import { connect } from 'react-redux';
import { Route, Switch, Redirect, useRouteMatch } from 'react-router-dom';
import { Route, Switch, Redirect, useRouteMatch, useLocation } from 'react-router-dom';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Modal, Button } from '@edx/paragon';
import { idVerificationSelector } from './data/selectors';
@@ -22,8 +22,17 @@ import messages from './IdVerification.messages';
// eslint-disable-next-line react/prefer-stateless-function
function IdVerificationPage(props) {
const { path } = useRouteMatch();
const { search } = useLocation();
const [isModalOpen, setIsModalOpen] = useState(false);
// Course run key is passed as a query string
useEffect(() => {
if (search) {
sessionStorage.setItem('courseRunKey', search.substring(1));
}
}, [search]);
return (
<>
{/* If user reloads, redirect to the beginning of the process */}
@@ -33,9 +42,6 @@ function IdVerificationPage(props) {
<div className="col-lg-6 col-md-8">
<IdVerificationContextProvider>
<Switch>
<Route exact path={path}>
<Redirect to={`${path}/review-requirements`} />
</Route>
<Route path={`${path}/review-requirements`} component={ReviewRequirementsPanel} />
<Route path={`${path}/request-camera-access`} component={RequestCameraAccessPanel} />
<Route path={`${path}/portrait-photo-context`} component={PortraitPhotoContextPanel} />

View File

@@ -1,27 +1,52 @@
import React, { useCallback } from 'react';
import React, { useCallback, useState } from 'react';
import { intlShape } from '@edx/frontend-platform/i18n';
import PropTypes from 'prop-types';
import { Alert } from '@edx/paragon';
import messages from './IdVerification.messages';
export default function ImageFileUpload({ onFileChange, intl }) {
const [fileTooLargeError, setFileTooLargeError] = useState(false);
const maxFileSize = 10000000;
export default function ImageFileUpload({ onFileChange }) {
const handleChange = useCallback((e) => {
if (e.target.files.length === 0) {
return;
}
const fileObject = e.target.files[0];
const fileReader = new FileReader();
fileReader.addEventListener('load', () => onFileChange(fileReader.result));
fileReader.readAsDataURL(fileObject);
if (fileObject.size < maxFileSize) {
const fileReader = new FileReader();
fileReader.addEventListener('load', () => onFileChange(fileReader.result));
fileReader.readAsDataURL(fileObject);
} else {
setFileTooLargeError(true);
}
}, []);
return (
<input
type="file"
accept="image/*"
onChange={handleChange}
/>
<>
<input
type="file"
accept="image/*"
data-testid="fileUpload"
onChange={handleChange}
/>
{fileTooLargeError && (
<Alert
id="fileTooLargeError"
variant="danger"
tabIndex="-1"
style={{ marginTop: '1rem' }}
>
{intl.formatMessage(messages['id.verification.id.photo.instructions.upload.error'])}
</Alert>
)}
</>
);
}
ImageFileUpload.propTypes = {
onFileChange: PropTypes.func.isRequired,
intl: intlShape.isRequired,
};

View File

@@ -5,7 +5,7 @@ export default function ImagePreview({ src, alt, id }) {
return (
<div id={id} className="image-preview">
<img style={{ objectFit: 'contain' }} src={src} alt={alt} />
<img data-hj-suppress style={{ objectFit: 'contain' }} src={src} alt={alt} />
</div>
);

View File

@@ -1,6 +1,6 @@
.page__id-verification {
.verification-panel {
.card.requirements {
.card.accent {
border-top: solid 4px theme-color('warning');
}
.image-preview {
@@ -12,6 +12,14 @@
max-height: 10rem;
}
}
.form-check {
padding: 0.5rem 0.5rem 1rem;
.form-check-label {
margin-left: 0.5rem;
padding-top: 0.2rem;
font-weight: normal;
}
}
}
.action-row {
display: flex;
@@ -44,6 +52,14 @@
width: 100%;
}
.canvas-video {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
}
.camera-btn {
margin: 10px;
}
@@ -60,4 +76,4 @@
opacity: 0;
background: white;
}
}
}

View File

@@ -45,6 +45,7 @@ export async function submitIdVerification(verificationData) {
facePhotoFile: 'face_image',
idPhotoFile: 'photo_id_image',
idPhotoName: 'full_name',
courseRunKey: 'course_key',
};
const postData = {};
// Don't include blank/null/undefined values.

View File

@@ -16,7 +16,7 @@ export default function BasePanel({
if (focusOnMount && headingRef.current) {
headingRef.current.focus();
}
}, []);
}, [headingRef.current]);
const redirectSlug = useVerificationRedirectSlug(name);
if (redirectSlug) {

View File

@@ -0,0 +1,71 @@
import PropTypes from 'prop-types';
import React from 'react';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import messages from '../IdVerification.messages';
export function EnableCameraDirectionsPanel(props) {
if (props.browserName === 'Internet Explorer') {
return (
<>
<h6>{props.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>
</ol>
</>
);
} else if (props.browserName === 'Chrome') {
return (
<>
<h6>{props.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>
<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>
</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>
</ol>
</>
);
} else if (props.browserName === 'Firefox') {
return (
<>
<h6>{props.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>
</ol>
</>
);
} else if (props.browserName === 'Safari') {
return (
<>
<h6>{props.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>
</ol>
</>
);
}
return <></>;
}
EnableCameraDirectionsPanel.propTypes = {
intl: intlShape.isRequired,
browserName: PropTypes.string.isRequired,
};
export default injectIntl(EnableCameraDirectionsPanel);

View File

@@ -1,83 +1,137 @@
import React, { useContext, useState, useEffect, useRef } from 'react';
import { Input, Button } from '@edx/paragon';
import { Link } from 'react-router-dom';
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
import { Form } from '@edx/paragon';
import { Link, useHistory } from 'react-router-dom';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useNextPanelSlug } from '../routing-utilities';
import BasePanel from './BasePanel';
import { IdVerificationContext } from '../IdVerificationContext';
import ImagePreview from '../ImagePreview';
import messages from '../IdVerification.messages';
function GetNameIdPanel(props) {
const { push } = useHistory();
const panelSlug = 'get-name-id';
const [isEditing, setIsEditing] = useState(false);
const [nameMatches, setNameMatches] = useState(true);
const nameInputRef = useRef();
const nextPanelSlug = useNextPanelSlug(panelSlug);
useEffect(() => {
if (isEditing && nameInputRef.current) {
nameInputRef.current.focus();
}
}, [isEditing]);
const {
nameOnAccount, idPhotoName, setIdPhotoName, idPhotoFile,
nameOnAccount, userId, idPhotoName, setIdPhotoName,
} = useContext(IdVerificationContext);
const nameOnAccountValue = nameOnAccount || '';
const invalidName = !nameMatches && (!idPhotoName || idPhotoName === nameOnAccount);
const blankName = !nameOnAccount && !idPhotoName;
useEffect(() => {
setIdPhotoName('');
}, []);
useEffect(() => {
if (!nameMatches && nameInputRef.current) {
nameInputRef.current.focus();
}
if (!nameMatches) {
sendTrackEvent('edx.id_verification.name_change', {
category: 'id_verification',
user_id: userId,
});
}
if (blankName) {
setNameMatches(false);
}
}, [nameMatches, blankName]);
const handleSubmit = (e) => {
e.preventDefault();
// If the input is empty, or if no changes have been made to the
// mismatching name, the user should not be able to proceed.
if (!invalidName && !blankName) {
push(nextPanelSlug);
}
};
return (
<BasePanel
name={panelSlug}
title="Account Name Check"
title={props.intl.formatMessage(messages['id.verification.account.name.title'])}
>
<p>
{props.intl.formatMessage(messages['id.verification.account.name.instructions'])}
</p>
<div className="alert alert-warning">
<FormattedMessage
id="id.verification.account.name.warning"
defaultMessage="{prefix} Any edit to your name will be saved to your account and can be reviewed on {accountSettings}."
description="Warning that any edit to the user's name will be saved to the account."
values={{
prefix: <strong>{props.intl.formatMessage(messages['id.verification.account.name.warning.prefix'])}</strong>,
accountSettings: <Link to="/">{props.intl.formatMessage(messages['id.verification.account.name.settings'])}</Link>,
}}
/>
</div>
<div className="form-group">
<label htmlFor="photo-id-name">
{props.intl.formatMessage(messages['id.verification.account.name.label'])}
</label>
<div className="d-flex">
<Input
id="photo-id-name"
<Form onSubmit={handleSubmit}>
<Form.Group>
<Form.Label htmlFor="nameMatchesYes">
{props.intl.formatMessage(messages['id.verification.account.name.radio.label'])}
</Form.Label>
<Form.Row>
<Form.Check
type="radio"
id="nameMatchesYes"
name="nameMatches"
data-testid="name-matches-yes"
label={props.intl.formatMessage(messages['id.verification.account.name.radio.yes'])}
checked={nameMatches}
disabled={!nameOnAccount}
inline
onChange={() => {
setNameMatches(true);
setIdPhotoName('');
}}
/>
<Form.Check
type="radio"
id="nameMatchesNo"
name="nameMatches"
data-testid="name-matches-no"
label={props.intl.formatMessage(messages['id.verification.account.name.radio.no'])}
checked={!nameMatches}
disabled={!nameOnAccount}
inline
onChange={() => setNameMatches(false)}
/>
</Form.Row>
</Form.Group>
<Form.Group>
<Form.Label htmlFor="photo-id-name">
{props.intl.formatMessage(messages['id.verification.account.name.label'])}
</Form.Label>
<Form.Control
controlId="photo-id-name"
size="lg"
type="text"
ref={nameInputRef}
disabled={!isEditing}
readOnly={!isEditing}
value={idPhotoName || nameOnAccountValue}
readOnly={nameMatches}
isInvalid={invalidName || blankName}
aria-describedby="photo-id-name-feedback"
value={
!nameMatches ?
idPhotoName || nameOnAccountValue
: nameOnAccountValue
}
onChange={e => setIdPhotoName(e.target.value)}
data-testid="name-input"
/>
{!isEditing && (
<Button
className="btn-link px-0 ml-3"
onClick={() => setIsEditing(true)}
>
{props.intl.formatMessage(messages['id.verification.account.name.edit'])}
</Button>
)}
</div>
</div>
<ImagePreview
id="photo-of-id"
src={idPhotoFile}
alt={props.intl.formatMessage(messages['id.verification.account.name.photo.alt'])}
/>
<Form.Control.Feedback id="photo-id-name-feedback" type="invalid">
{props.intl.formatMessage(messages['id.verification.account.name.error'])}
</Form.Control.Feedback>
</Form.Group>
</Form>
<div className="action-row">
<Link to={nextPanelSlug} className="btn btn-primary">
{isEditing ? props.intl.formatMessage(messages['id.verification.account.name.save']) : props.intl.formatMessage(messages['id.verification.next'])}
<Link
to={nextPanelSlug}
className={`btn btn-primary ${(invalidName || blankName) && 'disabled'}`}
data-testid="next-button"
aria-disabled={invalidName || blankName}
>
{
!nameMatches ?
props.intl.formatMessage(messages['id.verification.account.name.save'])
: props.intl.formatMessage(messages['id.verification.next'])
}
</Link>
</div>
</BasePanel>

View File

@@ -4,7 +4,7 @@ import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useNextPanelSlug } from '../routing-utilities';
import BasePanel from './BasePanel';
import CameraHelp from '../CameraHelp';
import messages from '../IdVerification.messages';
function IdContextPanel(props) {
@@ -16,7 +16,7 @@ function IdContextPanel(props) {
title={props.intl.formatMessage(messages['id.verification.id.tips.title'])}
>
<p>{props.intl.formatMessage(messages['id.verification.id.tips.description'])}</p>
<div className="card mb-4 shadow">
<div className="card mb-4 shadow accent">
<div className="card-body">
<h6>
{props.intl.formatMessage(messages['id.verification.photo.tips.list.title'])}
@@ -34,8 +34,9 @@ function IdContextPanel(props) {
</ul>
</div>
</div>
<CameraHelp isOpen />
<div className="action-row">
<Link to={nextPanelSlug} className="btn btn-primary">
<Link to={nextPanelSlug} className="btn btn-primary" data-testid="next-button">
{props.intl.formatMessage(messages['id.verification.next'])}
</Link>
</div>

View File

@@ -4,7 +4,7 @@ import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useNextPanelSlug } from '../routing-utilities';
import BasePanel from './BasePanel';
import CameraHelp from '../CameraHelp';
import messages from '../IdVerification.messages';
function PortraitPhotoContextPanel(props) {
@@ -18,7 +18,7 @@ function PortraitPhotoContextPanel(props) {
<p>
{props.intl.formatMessage(messages['id.verification.photo.tips.description'])}
</p>
<div className="card mb-4 shadow">
<div className="card mb-4 shadow accent">
<div className="card-body">
<h6>
{props.intl.formatMessage(messages['id.verification.photo.tips.list.title'])}
@@ -36,8 +36,9 @@ function PortraitPhotoContextPanel(props) {
</ul>
</div>
</div>
<CameraHelp isOpen isPortrait />
<div className="action-row">
<Link to={nextPanelSlug} className="btn btn-primary">
<Link to={nextPanelSlug} className="btn btn-primary" data-testid="next-button">
{props.intl.formatMessage(messages['id.verification.next'])}
</Link>
</div>

View File

@@ -1,23 +1,62 @@
import React, { useContext } from 'react';
import React, { useState, useEffect, useContext } from 'react';
import { Link } from 'react-router-dom';
import { Collapsible } from '@edx/paragon';
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 { useNextPanelSlug } from '../routing-utilities';
import BasePanel from './BasePanel';
import { IdVerificationContext, MEDIA_ACCESS } from '../IdVerificationContext';
import { EnableCameraDirectionsPanel } from './EnableCameraDirectionsPanel';
import messages from '../IdVerification.messages';
function RequestCameraAccessPanel(props) {
const [returnUrl, setReturnUrl] = useState('dashboard');
const [returnText, setReturnText] = useState('id.verification.return.dashboard');
const panelSlug = 'request-camera-access';
const nextPanelSlug = useNextPanelSlug(panelSlug);
const { tryGetUserMedia, mediaAccess } = useContext(IdVerificationContext);
const { tryGetUserMedia, mediaAccess, userId } = useContext(IdVerificationContext);
const browserName = Bowser.parse(window.navigator.userAgent).browser.name;
useEffect(() => {
if (mediaAccess === MEDIA_ACCESS.UNSUPPORTED) {
sendTrackEvent('edx.id_verification.camera.unsupported', {
category: 'id_verification',
user_id: userId,
});
}
if (mediaAccess === MEDIA_ACCESS.DENIED) {
sendTrackEvent('edx.id_verification.camera.denied', {
category: 'id_verification',
user_id: userId,
});
}
}, [mediaAccess, userId]);
// If the user accessed IDV through a course,
// link back to that course rather than the dashboard
useEffect(() => {
if (sessionStorage.getItem('courseRunKey')) {
setReturnUrl(`courses/${sessionStorage.getItem('courseRunKey')}`);
setReturnText('id.verification.return.course');
}
}, []);
const getTitle = () => {
if (mediaAccess === MEDIA_ACCESS.GRANTED) {
return props.intl.formatMessage(messages['id.verification.camera.access.title.success']);
} else if ([MEDIA_ACCESS.UNSUPPORTED, MEDIA_ACCESS.DENIED].includes(mediaAccess)) {
return props.intl.formatMessage(messages['id.verification.camera.access.title.failed']);
}
return props.intl.formatMessage(messages['id.verification.camera.access.title']);
};
return (
<BasePanel
name={panelSlug}
title={props.intl.formatMessage(messages['id.verification.camera.access.title'])}
title={getTitle()}
>
{mediaAccess === MEDIA_ACCESS.PENDING && (
<div>
@@ -35,29 +74,17 @@ function RequestCameraAccessPanel(props) {
<button className="btn btn-primary" onClick={tryGetUserMedia}>
{props.intl.formatMessage(messages['id.verification.camera.access.enable'])}
</button>
<Collapsible.Advanced className="mr-auto">
<Collapsible.Visible whenClosed>
<Collapsible.Trigger tag="button" className="btn btn-link px-0">
{props.intl.formatMessage(messages['id.verification.camera.access.problems'])}
</Collapsible.Trigger>
</Collapsible.Visible>
<Collapsible.Body>
<Link to={nextPanelSlug} className="btn btn-link">
{props.intl.formatMessage(messages['id.verification.camera.access.skip'])}
</Link>
</Collapsible.Body>
</Collapsible.Advanced>
</div>
</div>
)}
{mediaAccess === MEDIA_ACCESS.GRANTED && (
<div>
<p>
<p data-testid="camera-access-success">
{props.intl.formatMessage(messages['id.verification.camera.access.success'])}
</p>
<div className="action-row">
<Link to={nextPanelSlug} className="btn btn-primary">
<Link to={nextPanelSlug} className="btn btn-primary" data-testid="next-button">
{props.intl.formatMessage(messages['id.verification.next'])}
</Link>
</div>
@@ -65,14 +92,15 @@ function RequestCameraAccessPanel(props) {
)}
{[MEDIA_ACCESS.UNSUPPORTED, MEDIA_ACCESS.DENIED].includes(mediaAccess) && (
<div>
<p>
{props.intl.formatMessage(messages['id.verification.camera.access.failure'])}
<div data-testid="camera-failure-instructions">
<p data-testid="camera-access-failure">
{props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary'])}
</p>
<EnableCameraDirectionsPanel browserName={browserName} intl={props.intl} />
<div className="action-row">
<Link to={nextPanelSlug} className="btn btn-primary">
{props.intl.formatMessage(messages['id.verification.next'])}
</Link>
<a className="btn btn-primary" href={`${getConfig().LMS_BASE_URL}/${returnUrl}`}>
{props.intl.formatMessage(messages[returnText])}
</a>
</div>
</div>
)}

View File

@@ -1,27 +1,38 @@
import React from 'react';
import React, { useEffect, useContext } from 'react';
import { Link } from 'react-router-dom';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
import { useNextPanelSlug } from '../routing-utilities';
import BasePanel from './BasePanel';
import { IdVerificationContext } from '../IdVerificationContext';
import messages from '../IdVerification.messages';
function ReviewRequirementsPanel(props) {
const { userId } = useContext(IdVerificationContext);
const panelSlug = 'review-requirements';
const nextPanelSlug = useNextPanelSlug(panelSlug);
useEffect(() => {
sendTrackEvent('edx.id_verification.started', {
category: 'id_verification',
user_id: userId,
});
}, [userId]);
return (
<BasePanel
name={panelSlug}
title="Photo Verification Requirements"
title={props.intl.formatMessage(messages['id.verification.requirements.title'])}
focusOnMount={false}
>
<p>
{props.intl.formatMessage(messages['id.verification.requirements.description'])}
</p>
<div className="card mb-4 shadow requirements">
<div className="card mb-4 shadow accent">
<div className="card-body">
<h6>
<h6 aria-level="3">
{props.intl.formatMessage(messages['id.verification.requirements.card.device.title'])}
</h6>
<p className="mb-0">
@@ -36,9 +47,9 @@ function ReviewRequirementsPanel(props) {
</p>
</div>
</div>
<div className="card mb-4 shadow requirements">
<div className="card mb-4 shadow accent">
<div className="card-body">
<h6>
<h6 aria-level="3">
{props.intl.formatMessage(messages['id.verification.requirements.card.id.title'])}
</h6>
<p className="mb-0">
@@ -46,16 +57,16 @@ function ReviewRequirementsPanel(props) {
</p>
</div>
</div>
<h4 className="mb-3">
<h4 aria-level="2" className="mb-3">
{props.intl.formatMessage(messages['id.verification.privacy.title'])}
</h4>
<h6>
<h6 aria-level="3">
{props.intl.formatMessage(messages['id.verification.privacy.need.photo.question'])}
</h6>
<p>
{props.intl.formatMessage(messages['id.verification.privacy.need.photo.answer'])}
</p>
<h6>
<h6 aria-level="3">
{props.intl.formatMessage(messages['id.verification.privacy.do.with.photo.question'])}
</h6>
<p>
@@ -63,7 +74,7 @@ function ReviewRequirementsPanel(props) {
</p>
<div className="action-row">
<Link to={nextPanelSlug} className="btn btn-primary">
<Link to={nextPanelSlug} className="btn btn-primary" data-testid="next-button">
{props.intl.formatMessage(messages['id.verification.next'])}
</Link>
</div>

View File

@@ -1,13 +1,32 @@
import React from 'react';
import React, { useState, useEffect, useContext } from 'react';
import { getConfig } from '@edx/frontend-platform';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import BasePanel from './BasePanel';
import { IdVerificationContext } from '../IdVerificationContext';
import messages from '../IdVerification.messages';
function SubmittedPanel(props) {
const { userId } = useContext(IdVerificationContext);
const [returnUrl, setReturnUrl] = useState('dashboard');
const [returnText, setReturnText] = useState('id.verification.return.dashboard');
const panelSlug = 'submitted';
// If the user accessed IDV through a course,
// link back to that course rather than the dashboard
useEffect(() => {
if (sessionStorage.getItem('courseRunKey')) {
setReturnUrl(`courses/${sessionStorage.getItem('courseRunKey')}`);
setReturnText('id.verification.return.course');
}
sendTrackEvent('edx.id_verification.submitted', {
category: 'id_verification',
user_id: userId,
});
}, [userId]);
return (
<BasePanel
name={panelSlug}
@@ -16,8 +35,12 @@ function SubmittedPanel(props) {
<p>
{props.intl.formatMessage(messages['id.verification.submitted.text'])}
</p>
<a className="btn btn-primary" href={`${getConfig().LMS_BASE_URL}/dashboard`}>
{props.intl.formatMessage(messages['id.verification.return'])}
<a
className="btn btn-primary"
href={`${getConfig().LMS_BASE_URL}/${returnUrl}`}
data-testid="return-button"
>
{props.intl.formatMessage(messages[returnText])}
</a>
</BasePanel>
);

View File

@@ -1,8 +1,8 @@
import React, { useContext } from 'react';
import React, { useState, useContext } from 'react';
import { history } from '@edx/frontend-platform';
import { Input, Button } from '@edx/paragon';
import { Input, Button, Spinner, Alert } from '@edx/paragon';
import { Link } from 'react-router-dom';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
import { submitIdVerification } from '../data/service';
import { useNextPanelSlug } from '../routing-utilities';
@@ -11,6 +11,7 @@ import { IdVerificationContext } from '../IdVerificationContext';
import ImagePreview from '../ImagePreview';
import messages from '../IdVerification.messages';
import CameraHelpWithUpload from '../CameraHelpWithUpload';
function SummaryPanel(props) {
const panelSlug = 'summary';
@@ -20,23 +21,39 @@ function SummaryPanel(props) {
idPhotoFile,
nameOnAccount,
idPhotoName,
stopUserMedia,
} = useContext(IdVerificationContext);
const nameToBeUsed = idPhotoName || nameOnAccount || '';
const [isSubmitting, setIsSubmitting] = useState(false);
const [submissionError, setSubmissionError] = useState(false);
function SubmitButton() {
async function handleClick() {
setIsSubmitting(true);
const verificationData = {
facePhotoFile,
idPhotoFile,
idPhotoName: nameToBeUsed,
courseRunKey: sessionStorage.getItem('courseRunKey'),
};
const result = await submitIdVerification(verificationData);
if (result.success) {
stopUserMedia();
history.push(nextPanelSlug);
} else {
stopUserMedia();
setIsSubmitting(false);
setSubmissionError(true);
}
}
return (
<Button className="btn btn-primary" title="Confirmation" onClick={handleClick}>
<Button
className="btn btn-primary"
title="Confirmation"
disabled={isSubmitting}
onClick={handleClick}
data-testid="submit-button"
>
{props.intl.formatMessage(messages['id.verification.review.confirm'])}
</Button>
);
@@ -47,6 +64,24 @@ function SummaryPanel(props) {
name={panelSlug}
title={props.intl.formatMessage(messages['id.verification.review.title'])}
>
{submissionError &&
<Alert
variant="danger"
data-testid="submission-error"
dismissible
onClose={() => setSubmissionError(false)}
>
<FormattedMessage
id="idv.submission.alert.error"
defaultMessage={`
We encountered a technical error while trying to submit ID verification.
This might be a temporary issue, so please try again in a few minutes.
If the problem persists,
please go to {support_link} for help.
`}
values={{ support_link: <Alert.Link href="https://support.edx.org/hc/en-us">{props.intl.formatMessage(messages['id.verification.review.error'])}</Alert.Link> }}
/>
</Alert>}
<p>
{props.intl.formatMessage(messages['id.verification.review.description'])}
</p>
@@ -61,11 +96,12 @@ function SummaryPanel(props) {
alt={props.intl.formatMessage(messages['id.verification.review.portrait.alt'])}
/>
<Link
className="btn btn-inverse-primary shadow"
className="btn btn-outline-primary"
to={{
pathname: 'take-portrait-photo',
state: { fromSummary: true },
}}
data-testid="portrait-retake"
>
{props.intl.formatMessage(messages['id.verification.review.portrait.retake'])}
</Link>
@@ -80,16 +116,18 @@ function SummaryPanel(props) {
alt={props.intl.formatMessage(messages['id.verification.review.id.alt'])}
/>
<Link
className="btn btn-inverse-primary shadow"
className="btn btn-outline-primary"
to={{
pathname: 'take-id-photo',
state: { fromSummary: true },
}}
data-testid="id-retake"
>
{props.intl.formatMessage(messages['id.verification.review.id.retake'])}
</Link>
</div>
</div>
<CameraHelpWithUpload />
<div className="form-group">
<label htmlFor="name-to-be-used">
{props.intl.formatMessage(messages['id.verification.account.name.label'])}
@@ -110,11 +148,19 @@ function SummaryPanel(props) {
state: { fromSummary: true },
}}
>
{props.intl.formatMessage(messages['id.verification.account.name.edit'])}
<FormattedMessage
id="id.verification.account.name.edit"
defaultMessage="Edit{sr}"
description="Button to edit account name, with clarifying information for screen readers."
values={{
sr: <span className="sr-only">Account Name</span>,
}}
/>
</Link>
</div>
</div>
<SubmitButton />
<SubmitButton />{' '}
{isSubmitting && <Spinner animation="border" variant="primary" />}
</BasePanel>
);
}

View File

@@ -4,49 +4,34 @@ import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useNextPanelSlug } from '../routing-utilities';
import BasePanel from './BasePanel';
import ImageFileUpload from '../ImageFileUpload';
import ImagePreview from '../ImagePreview';
import Camera from '../Camera';
import CameraHelp from '../CameraHelp';
import { IdVerificationContext, MEDIA_ACCESS } from '../IdVerificationContext';
import { IdVerificationContext } from '../IdVerificationContext';
import messages from '../IdVerification.messages';
import CameraHelp from '../CameraHelp';
function TakeIdPhotoPanel(props) {
const panelSlug = 'take-id-photo';
const nextPanelSlug = useNextPanelSlug(panelSlug);
const { setIdPhotoFile, idPhotoFile, mediaAccess } = useContext(IdVerificationContext);
const shouldUseCamera = mediaAccess === MEDIA_ACCESS.GRANTED;
const { setIdPhotoFile, idPhotoFile } = useContext(IdVerificationContext);
return (
<BasePanel
name={panelSlug}
title={shouldUseCamera ? props.intl.formatMessage(messages['id.verification.id.photo.title.camera']) : props.intl.formatMessage(messages['id.verification.id.photo.title.upload'])}
title={props.intl.formatMessage(messages['id.verification.id.photo.title.camera'])}
>
<div>
{idPhotoFile && !shouldUseCamera && <ImagePreview src={idPhotoFile} alt={props.intl.formatMessage(messages['id.verification.id.photo.preview.alt'])} />}
{shouldUseCamera ? (
<div>
<p>
{props.intl.formatMessage(messages['id.verification.id.photo.instructions.camera'])}
</p>
<Camera onImageCapture={setIdPhotoFile} />
</div>
) : (
<div>
<p>
{props.intl.formatMessage(messages['id.verification.id.photo.instructions.upload'])}
</p>
<ImageFileUpload onFileChange={setIdPhotoFile} />
</div>
)}
<p>
{props.intl.formatMessage(messages['id.verification.id.photo.instructions.camera'])}
</p>
<Camera onImageCapture={setIdPhotoFile} isPortrait={false} />
</div>
<CameraHelp />
<div className="action-row" style={{ visibility: idPhotoFile ? 'unset' : 'hidden' }}>
<Link to={nextPanelSlug} className="btn btn-primary">
<Link to={nextPanelSlug} className="btn btn-primary" data-testid="next-button">
{props.intl.formatMessage(messages['id.verification.next'])}
</Link>
</div>
{shouldUseCamera && <CameraHelp />}
</BasePanel>
);
}

View File

@@ -8,15 +8,17 @@ import ImageFileUpload from '../ImageFileUpload';
import ImagePreview from '../ImagePreview';
import Camera from '../Camera';
import CameraHelp from '../CameraHelp';
import { IdVerificationContext, MEDIA_ACCESS } from '../IdVerificationContext';
import { IdVerificationContext } from '../IdVerificationContext';
import messages from '../IdVerification.messages';
function TakePortraitPhotoPanel(props) {
const panelSlug = 'take-portrait-photo';
const nextPanelSlug = useNextPanelSlug(panelSlug);
const { setFacePhotoFile, facePhotoFile, mediaAccess } = useContext(IdVerificationContext);
const shouldUseCamera = mediaAccess === MEDIA_ACCESS.GRANTED;
const { setFacePhotoFile, facePhotoFile } = useContext(IdVerificationContext);
const shouldUseCamera = true;
// to reenable upload component:
// const shouldUseCamera = mediaAccess === MEDIA_ACCESS.GRANTED;
return (
<BasePanel
@@ -31,7 +33,7 @@ function TakePortraitPhotoPanel(props) {
<p>
{props.intl.formatMessage(messages['id.verification.portrait.photo.instructions.camera'])}
</p>
<Camera onImageCapture={setFacePhotoFile} />
<Camera onImageCapture={setFacePhotoFile} isPortrait />
</div>
) : (
<div>
@@ -42,12 +44,12 @@ function TakePortraitPhotoPanel(props) {
</div>
)}
</div>
{shouldUseCamera && <CameraHelp isPortrait />}
<div className="action-row" style={{ visibility: facePhotoFile ? 'unset' : 'hidden' }}>
<Link to={nextPanelSlug} className="btn btn-primary">
<Link to={nextPanelSlug} className="btn btn-primary" data-testid="next-button">
{props.intl.formatMessage(messages['id.verification.next'])}
</Link>
</div>
{shouldUseCamera && <CameraHelp />}
</BasePanel>
);
}

View File

@@ -0,0 +1,184 @@
import React from 'react';
import { Router } from 'react-router-dom';
import { createMemoryHistory } from 'history';
import '@testing-library/jest-dom/extend-expect';
import { render, cleanup, screen, act, fireEvent } from '@testing-library/react';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import * as blazeface from '@tensorflow-models/blazeface';
import * as analytics from '@edx/frontend-platform/analytics';
import { IdVerificationContext } from '../IdVerificationContext';
import Camera from '../Camera';
jest.mock('jslib-html5-camera-photo');
jest.mock('@tensorflow-models/blazeface');
jest.mock('@edx/frontend-platform/analytics');
analytics.sendTrackEvent = jest.fn();
window.HTMLMediaElement.prototype.play = () => {};
const IntlCamera = injectIntl(Camera);
const history = createMemoryHistory();
describe('SubmittedPanel', () => {
const defaultProps = {
intl: {},
onImageCapture: jest.fn(),
isPortrait: true,
};
const idProps = {
intl: {},
onImageCapture: jest.fn(),
isPortrait: false,
};
const contextValue = {};
afterEach(() => {
cleanup();
});
it('takes photo', async () => {
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlCamera {...defaultProps} />
</IdVerificationContext.Provider>
</IntlProvider>
</Router>
)));
const button = await screen.findByRole('button');
expect(button).toHaveTextContent('Take Photo');
fireEvent.click(button);
expect(defaultProps.onImageCapture).toHaveBeenCalled();
});
it('shows correct help text for portrait photo capture', async () => {
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlCamera {...defaultProps} />
</IdVerificationContext.Provider>
</IntlProvider>
</Router>
)));
const helpText = screen.getByTestId('videoDetectionHelpText');
expect(helpText.textContent).toEqual(expect.stringContaining('Your face can be seen clearly'));
});
it('shows correct help text for id photo capture', async () => {
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlCamera {...idProps} />
</IdVerificationContext.Provider>
</IntlProvider>
</Router>
)));
const helpText = screen.getByTestId('videoDetectionHelpText');
expect(helpText.textContent).toEqual(expect.stringContaining('The face can be seen clearly'));
});
it('shows spinner when loading face detection', async () => {
blazeface.load = jest.fn().mockResolvedValue({ estimateFaces: jest.fn().mockResolvedValue([]) });
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlCamera {...defaultProps} />
</IdVerificationContext.Provider>
</IntlProvider>
</Router>
)));
await fireEvent.loadedData(screen.queryByTestId('video'));
const checkbox = await screen.findByLabelText('Enable Face Detection');
fireEvent.click(checkbox);
expect(screen.queryByTestId('spinner')).toBeDefined();
});
it('canvas is visible when detection is enabled', async () => {
blazeface.load = jest.fn().mockResolvedValue({ estimateFaces: jest.fn().mockResolvedValue([]) });
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlCamera {...defaultProps} />
</IdVerificationContext.Provider>
</IntlProvider>
</Router>
)));
await fireEvent.loadedData(screen.queryByTestId('video'));
expect(screen.queryByTestId('detection-canvas')).toHaveStyle('display:none');
const checkbox = await screen.findByLabelText('Enable Face Detection');
await fireEvent.click(checkbox);
expect(screen.queryByTestId('detection-canvas')).toHaveStyle('display:block');
});
it('blazeface is called when detection is enabled', async () => {
blazeface.load = jest.fn().mockResolvedValue({ estimateFaces: jest.fn().mockResolvedValue([]) });
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlCamera {...defaultProps} />
</IdVerificationContext.Provider>
</IntlProvider>
</Router>
)));
await fireEvent.loadedData(screen.queryByTestId('video'));
const checkbox = await screen.findByLabelText('Enable Face Detection');
await fireEvent.click(checkbox);
setTimeout(() => { expect(blazeface.load).toHaveBeenCalled(); }, 2000);
});
it('sends tracking events on portrait photo page', async () => {
blazeface.load = jest.fn().mockResolvedValue({ estimateFaces: jest.fn().mockResolvedValue([]) });
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlCamera {...defaultProps} />
</IdVerificationContext.Provider>
</IntlProvider>
</Router>
)));
await fireEvent.loadedData(screen.queryByTestId('video'));
const checkbox = await screen.findByLabelText('Enable Face Detection');
await fireEvent.click(checkbox);
expect(analytics.sendTrackEvent).toHaveBeenCalledWith('edx.id_verification.user_photo.face_detection_enabled');
await fireEvent.click(checkbox);
expect(analytics.sendTrackEvent).toHaveBeenCalledWith('edx.id_verification.user_photo.face_detection_disabled');
});
it('sends tracking events on id photo page', async () => {
blazeface.load = jest.fn().mockResolvedValue({ estimateFaces: jest.fn().mockResolvedValue([]) });
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlCamera {...idProps} />
</IdVerificationContext.Provider>
</IntlProvider>
</Router>
)));
await fireEvent.loadedData(screen.queryByTestId('video'));
const checkbox = await screen.findByLabelText('Enable Face Detection');
await fireEvent.click(checkbox);
expect(analytics.sendTrackEvent).toHaveBeenCalledWith('edx.id_verification.id_photo.face_detection_enabled');
await fireEvent.click(checkbox);
expect(analytics.sendTrackEvent).toHaveBeenCalledWith('edx.id_verification.id_photo.face_detection_disabled');
});
});

View File

@@ -0,0 +1,71 @@
import React from 'react';
import { Router } from 'react-router-dom';
import { createMemoryHistory } from 'history';
import { render, cleanup, act, screen } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import '@edx/frontend-platform/analytics';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import ExistingRequest from '../ExistingRequest';
const IntlExistingRequest = injectIntl(ExistingRequest);
const history = createMemoryHistory();
describe('ExistingRequest', () => {
const defaultProps = {
intl: {},
status: '',
};
afterEach(() => {
cleanup();
});
it('renders correctly when status is pending', async () => {
defaultProps.status = 'pending';
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
<IntlExistingRequest {...defaultProps} />
</IntlProvider>
</Router>
)));
const text = screen.getByText(/You have already submitted your verification information./);
expect(text).toBeInTheDocument();
});
it('renders correctly when status is approved', async () => {
defaultProps.status = 'approved';
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
<IntlExistingRequest {...defaultProps} />
</IntlProvider>
</Router>
)));
const text = screen.getByText(/You have already submitted your verification information./);
expect(text).toBeInTheDocument();
});
it('renders correctly when status is denied', async () => {
defaultProps.status = 'denied';
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
<IntlExistingRequest {...defaultProps} />
</IntlProvider>
</Router>
)));
const text = screen.getByText(/You cannot verify your identity at this time./);
expect(text).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,33 @@
import React from 'react';
import { render, cleanup, act } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { AppContext } from '@edx/frontend-platform/react';
import { getExistingIdVerification } from '../data/service';
import { IdVerificationContextProvider } from '../IdVerificationContext';
jest.mock('../data/service', () => ({
getExistingIdVerification: jest.fn(),
}));
describe('IdVerificationContext', () => {
const defaultProps = {
children: <div />,
intl: {},
};
afterEach(() => {
cleanup();
});
it('renders correctly and calls getExistingIdVerification', async () => {
await act(async () => render((
<AppContext.Provider value={{ authenticatedUser: { userId: 3 } }}>
<IntlProvider locale="en">
<IdVerificationContextProvider {...defaultProps} />
</IntlProvider>
</AppContext.Provider>
)));
expect(getExistingIdVerification).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,96 @@
import React from 'react';
import { Router } from 'react-router-dom';
import { createMemoryHistory } from 'history';
import { render, cleanup, act, screen, fireEvent } from '@testing-library/react';
import '@edx/frontend-platform/analytics';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import { IdVerificationContext } from '../../IdVerificationContext';
import GetNameIdPanel from '../../panels/GetNameIdPanel';
jest.mock('@edx/frontend-platform/analytics', () => ({
sendTrackEvent: jest.fn(),
}));
const IntlGetNameIdPanel = injectIntl(GetNameIdPanel);
const history = createMemoryHistory();
describe('GetNameIdPanel', () => {
const defaultProps = {
intl: {},
};
const contextValue = {
nameOnAccount: 'test',
userId: 3,
idPhotoName: '',
setIdPhotoName: jest.fn(),
facePhotoFile: 'test.jpg',
idPhotoFile: 'test.jpg',
};
afterEach(() => {
cleanup();
});
it('edits', async () => {
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlGetNameIdPanel {...defaultProps} />
</IdVerificationContext.Provider>
</IntlProvider>
</Router>
)));
const yesButton = await screen.findByTestId('name-matches-yes');
const noButton = await screen.findByTestId('name-matches-no');
const input = await screen.findByTestId('name-input');
const nextButton = await screen.findByTestId('next-button');
expect(input).toHaveProperty('readOnly');
fireEvent.click(noButton);
expect(input).toHaveProperty('readOnly', false);
expect(nextButton.classList.contains('disabled')).toBe(true);
fireEvent.change(input, { target: { value: 'test change' } });
expect(contextValue.setIdPhotoName).toHaveBeenCalled();
fireEvent.click(yesButton);
expect(input).toHaveProperty('readOnly');
expect(contextValue.setIdPhotoName).toHaveBeenCalled();
});
it('disables radio buttons + next button and enables input if account name is blank', async () => {
contextValue.nameOnAccount = '';
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlGetNameIdPanel {...defaultProps} />
</IdVerificationContext.Provider>
</IntlProvider>
</Router>
)));
const yesButton = await screen.findByTestId('name-matches-yes');
const noButton = await screen.findByTestId('name-matches-no');
const input = await screen.findByTestId('name-input');
const nextButton = await screen.findByTestId('next-button');
expect(yesButton).toHaveProperty('disabled');
expect(noButton).toHaveProperty('disabled');
expect(input).toHaveProperty('readOnly', false);
expect(nextButton.classList.contains('disabled')).toBe(true);
});
it('routes to SummaryPanel', async () => {
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlGetNameIdPanel {...defaultProps} />
</IdVerificationContext.Provider>
</IntlProvider>
</Router>
)));
const button = await screen.findByTestId('next-button');
fireEvent.click(button);
expect(history.location.pathname).toEqual('/summary');
});
});

View File

@@ -0,0 +1,45 @@
import React from 'react';
import { Router } from 'react-router-dom';
import { createMemoryHistory } from 'history';
import { render, cleanup, act, screen, fireEvent } from '@testing-library/react';
import '@edx/frontend-platform/analytics';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import { IdVerificationContext } from '../../IdVerificationContext';
import IdContextPanel from '../../panels/IdContextPanel';
jest.mock('@edx/frontend-platform/analytics', () => ({
sendTrackEvent: jest.fn(),
}));
const IntlIdContextPanel = injectIntl(IdContextPanel);
const history = createMemoryHistory();
describe('IdContextPanel', () => {
const defaultProps = {
intl: {},
};
const contextValue = {
facePhotoFile: 'test.jpg',
};
afterEach(() => {
cleanup();
});
it('routes to TakeIdPhotoPanel', async () => {
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlIdContextPanel {...defaultProps} />
</IdVerificationContext.Provider>
</IntlProvider>
</Router>
)));
const button = await screen.findByTestId('next-button');
fireEvent.click(button);
expect(history.location.pathname).toEqual('/take-id-photo');
});
});

View File

@@ -0,0 +1,38 @@
import React from 'react';
import { Router } from 'react-router-dom';
import { createMemoryHistory } from 'history';
import { render, cleanup, act, screen, fireEvent } from '@testing-library/react';
import '@edx/frontend-platform/analytics';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import PortraitPhotoContextPanel from '../../panels/PortraitPhotoContextPanel';
jest.mock('@edx/frontend-platform/analytics', () => ({
sendTrackEvent: jest.fn(),
}));
const IntlPortraitPhotoContextPanel = injectIntl(PortraitPhotoContextPanel);
const history = createMemoryHistory();
describe('PortraitPhotoContextPanel', () => {
const defaultProps = {
intl: {},
};
afterEach(() => {
cleanup();
});
it('routes to TakePortraitPhotoPanel', async () => {
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
<IntlPortraitPhotoContextPanel {...defaultProps} />
</IntlProvider>
</Router>
)));
const button = await screen.findByTestId('next-button');
fireEvent.click(button);
expect(history.location.pathname).toEqual('/take-portrait-photo');
});
});

View File

@@ -0,0 +1,164 @@
import React from 'react';
import { Router } from 'react-router-dom';
import Bowser from 'bowser';
import { createMemoryHistory } from 'history';
import { render, screen, cleanup, act, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import { IdVerificationContext } from '../../IdVerificationContext';
import RequestCameraAccessPanel from '../../panels/RequestCameraAccessPanel';
jest.mock('@edx/frontend-platform/analytics', () => ({
sendTrackEvent: jest.fn(),
}));
jest.mock('bowser');
const history = createMemoryHistory();
const IntlRequestCameraAccessPanel = injectIntl(RequestCameraAccessPanel);
describe('RequestCameraAccessPanel', () => {
const defaultProps = {
intl: {},
};
const contextValue = {
tryGetUserMedia: jest.fn(),
};
afterEach(() => {
cleanup();
});
it('renders correctly with media access pending', async () => {
contextValue.mediaAccess = 'pending';
Bowser.parse = jest.fn().mockReturnValue({ browser: { name: '' } });
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlRequestCameraAccessPanel {...defaultProps} />
</IdVerificationContext.Provider>
</IntlProvider>
</Router>
)));
const button = await screen.getByRole('button');
expect(button).toHaveTextContent(/Enable Camera/);
});
it('renders correctly with media access granted and routes to PortraitPhotoContextPanel', async () => {
contextValue.mediaAccess = 'granted';
Bowser.parse = jest.fn().mockReturnValue({ browser: { name: '' } });
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlRequestCameraAccessPanel {...defaultProps} />
</IdVerificationContext.Provider>
</IntlProvider>
</Router>
)));
const text = await screen.findByTestId('camera-access-success');
expect(text).toHaveTextContent(/Looks like your camera is working and ready./);
const button = await screen.findByTestId('next-button');
fireEvent.click(button);
expect(history.location.pathname).toEqual('/portrait-photo-context');
});
it('renders correctly with media access denied', async () => {
contextValue.mediaAccess = 'denied';
Bowser.parse = jest.fn().mockReturnValue({ browser: { name: '' } });
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlRequestCameraAccessPanel {...defaultProps} />
</IdVerificationContext.Provider>
</IntlProvider>
</Router>
)));
const text = await screen.findByTestId('camera-access-failure');
expect(text).toHaveTextContent(/It looks like we're unable to access your camera./);
});
it('renders correctly with media access unsupported', async () => {
contextValue.mediaAccess = 'unsupported';
Bowser.parse = jest.fn().mockReturnValue({ browser: { name: '' } });
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlRequestCameraAccessPanel {...defaultProps} />
</IdVerificationContext.Provider>
</IntlProvider>
</Router>
)));
const text = await screen.findByTestId('camera-access-failure');
expect(text).toHaveTextContent(/It looks like we're unable to access your camera./);
});
it('renders correct directions for Chrome with media access denied', async () => {
contextValue.mediaAccess = 'denied';
Bowser.parse = jest.fn().mockReturnValue({ browser: { name: 'Chrome' } });
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlRequestCameraAccessPanel {...defaultProps} />
</IdVerificationContext.Provider>
</IntlProvider>
</Router>
)));
const text = await screen.findByTestId('camera-failure-instructions');
expect(text).toHaveTextContent(/Open Chrome./);
});
it('renders correct directions for Firefox with media access denied', async () => {
contextValue.mediaAccess = 'denied';
Bowser.parse = jest.fn().mockReturnValue({ browser: { name: 'Firefox' } });
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlRequestCameraAccessPanel {...defaultProps} />
</IdVerificationContext.Provider>
</IntlProvider>
</Router>
)));
const text = await screen.findByTestId('camera-failure-instructions');
expect(text).toHaveTextContent(/Open Firefox./);
});
it('renders correct directions for Safari with media access denied', async () => {
contextValue.mediaAccess = 'denied';
Bowser.parse = jest.fn().mockReturnValue({ browser: { name: 'Safari' } });
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlRequestCameraAccessPanel {...defaultProps} />
</IdVerificationContext.Provider>
</IntlProvider>
</Router>
)));
const text = await screen.findByTestId('camera-failure-instructions');
expect(text).toHaveTextContent(/Open Safari./);
});
it('renders correct directions for IE11 with media access denied', async () => {
contextValue.mediaAccess = 'denied';
Bowser.parse = jest.fn().mockReturnValue({ browser: { name: 'Internet Explorer' } });
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlRequestCameraAccessPanel {...defaultProps} />
</IdVerificationContext.Provider>
</IntlProvider>
</Router>
)));
const text = await screen.findByTestId('camera-failure-instructions');
expect(text).toHaveTextContent(/Open the Flash Player/);
});
});

View File

@@ -0,0 +1,38 @@
import React from 'react';
import { Router } from 'react-router-dom';
import { createMemoryHistory } from 'history';
import { render, cleanup, act, screen, fireEvent } from '@testing-library/react';
import '@edx/frontend-platform/analytics';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import ReviewRequirementsPanel from '../../panels/ReviewRequirementsPanel';
jest.mock('@edx/frontend-platform/analytics', () => ({
sendTrackEvent: jest.fn(),
}));
const IntlReviewRequirementsPanel = injectIntl(ReviewRequirementsPanel);
const history = createMemoryHistory();
describe('ReviewRequirementsPanel', () => {
const defaultProps = {
intl: {},
};
afterEach(() => {
cleanup();
});
it('routes to RequestCameraAccessPanel', async () => {
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
<IntlReviewRequirementsPanel {...defaultProps} />
</IntlProvider>
</Router>
)));
const button = await screen.findByTestId('next-button');
fireEvent.click(button);
expect(history.location.pathname).toEqual('/request-camera-access');
});
});

View File

@@ -0,0 +1,65 @@
import React from 'react';
import { Router } from 'react-router-dom';
import { createMemoryHistory } from 'history';
import '@testing-library/jest-dom/extend-expect';
import { render, cleanup, act, screen } from '@testing-library/react';
import '@edx/frontend-platform/analytics';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import { IdVerificationContext } from '../../IdVerificationContext';
import SubmittedPanel from '../../panels/SubmittedPanel';
jest.mock('@edx/frontend-platform/analytics', () => ({
sendTrackEvent: jest.fn(),
}));
const IntlSubmittedPanel = injectIntl(SubmittedPanel);
const history = createMemoryHistory();
describe('SubmittedPanel', () => {
const defaultProps = {
intl: {},
};
const contextValue = {
facePhotoFile: 'test.jpg',
idPhotoFile: 'test.jpg',
};
beforeEach(() => {
global.sessionStorage.getItem = jest.fn();
});
afterEach(() => {
cleanup();
});
it('links to dashboard without courseRunKey', async () => {
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlSubmittedPanel {...defaultProps} />
</IdVerificationContext.Provider>
</IntlProvider>
</Router>
)));
const button = await screen.findByTestId('return-button');
expect(button).toHaveTextContent(/Return to Your Dashboard/);
});
it('links to course with courseRunKey', async () => {
Storage.prototype.getItem = jest.fn(() => 'courseRunKey');
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlSubmittedPanel {...defaultProps} />
</IdVerificationContext.Provider>
</IntlProvider>
</Router>
)));
const button = await screen.findByTestId('return-button');
expect(button).toHaveTextContent(/Return to Course/);
});
});

View File

@@ -0,0 +1,98 @@
import React from 'react';
import { Router } from 'react-router-dom';
import { createMemoryHistory } from 'history';
import { render, cleanup, act, screen, fireEvent, waitFor } from '@testing-library/react';
import '@edx/frontend-platform/analytics';
import '@testing-library/jest-dom/extend-expect';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import * as dataService from '../../data/service';
import { IdVerificationContext } from '../../IdVerificationContext';
import SummaryPanel from '../../panels/SummaryPanel';
jest.mock('@edx/frontend-platform/analytics', () => ({
sendTrackEvent: jest.fn(),
}));
jest.mock('../../data/service');
dataService.submitIdVerification = jest.fn().mockReturnValue({ success: true });
const IntlSummaryPanel = injectIntl(SummaryPanel);
const history = createMemoryHistory();
describe('SummaryPanel', () => {
const defaultProps = {
intl: {},
};
const contextValue = {
facePhotoFile: 'test.jpg',
idPhotoFile: 'test.jpg',
nameOnAccount: '',
idPhotoName: '',
stopUserMedia: jest.fn(),
};
beforeEach(async () => {
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlSummaryPanel {...defaultProps} />
</IdVerificationContext.Provider>
</IntlProvider>
</Router>
)));
});
afterEach(() => {
cleanup();
});
it('routes back to TakePortraitPhotoPanel', async () => {
const button = await screen.findByTestId('portrait-retake');
fireEvent.click(button);
expect(history.location.pathname).toEqual('/take-portrait-photo');
expect(history.location.state.fromSummary).toEqual(true);
});
it('routes back to TakeIdPhotoPanel', async () => {
const button = await screen.findByTestId('id-retake');
fireEvent.click(button);
expect(history.location.pathname).toEqual('/take-id-photo');
expect(history.location.state.fromSummary).toEqual(true);
});
it('allows user to upload ID photo', async () => {
const collapsible = await screen.getAllByRole('button', { 'aria-expanded': false })[0];
fireEvent.click(collapsible);
const uploadButton = await screen.getByTestId('fileUpload');
expect(uploadButton).toBeVisible();
});
it('submits', async () => {
const button = await screen.findByTestId('submit-button');
fireEvent.click(button);
expect(dataService.submitIdVerification).toHaveBeenCalled();
await waitFor(() => expect(contextValue.stopUserMedia).toHaveBeenCalled());
});
it('shows error when cannot submit', async () => {
await cleanup();
dataService.submitIdVerification = jest.fn().mockReturnValue({ success: false });
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlSummaryPanel {...defaultProps} />
</IdVerificationContext.Provider>
</IntlProvider>
</Router>
)));
const button = await screen.findByTestId('submit-button');
await act(async () => fireEvent.click(button));
expect(dataService.submitIdVerification).toHaveBeenCalled();
const error = await screen.getByTestId('submission-error');
expect(error).toBeDefined();
});
});

View File

@@ -0,0 +1,82 @@
import React from 'react';
import { Router } from 'react-router-dom';
import { createMemoryHistory } from 'history';
import { render, cleanup, act, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import { IdVerificationContext } from '../../IdVerificationContext';
import TakeIdPhotoPanel from '../../panels/TakeIdPhotoPanel';
jest.mock('@edx/frontend-platform/analytics', () => ({
sendTrackEvent: jest.fn(),
}));
jest.mock('../../Camera');
const history = createMemoryHistory();
const IntlTakeIdPhotoPanel = injectIntl(TakeIdPhotoPanel);
describe('TakeIdPhotoPanel', () => {
const defaultProps = {
intl: {},
};
const contextValue = {
facePhotoFile: 'test.jpg',
idPhotoFile: null,
setIdPhotoFile: jest.fn(),
};
afterEach(() => {
cleanup();
});
it('doesn\'t show next button before photo is taken', async () => {
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlTakeIdPhotoPanel {...defaultProps} />
</IdVerificationContext.Provider>
</IntlProvider>
</Router>
)));
const button = await screen.findByTestId('next-button');
expect(button).not.toBeVisible();
});
it('shows next button after photo is taken and routes to GetNameIdPanel', async () => {
contextValue.idPhotoFile = 'test.jpg';
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlTakeIdPhotoPanel {...defaultProps} />
</IdVerificationContext.Provider>
</IntlProvider>
</Router>
)));
const button = await screen.findByTestId('next-button');
expect(button).toBeVisible();
fireEvent.click(button);
expect(history.location.pathname).toEqual('/get-name-id');
});
it('routes back to SummaryPanel if that was the source', async () => {
contextValue.idPhotoFile = 'test.jpg';
history.location.state = { fromSummary: true };
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlTakeIdPhotoPanel {...defaultProps} />
</IdVerificationContext.Provider>
</IntlProvider>
</Router>
)));
const button = await screen.findByTestId('next-button');
fireEvent.click(button);
expect(history.location.pathname).toEqual('/summary');
});
});

View File

@@ -0,0 +1,81 @@
import React from 'react';
import { Router } from 'react-router-dom';
import { createMemoryHistory } from 'history';
import { render, cleanup, act, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import { IdVerificationContext } from '../../IdVerificationContext';
import TakePortraitPhotoPanel from '../../panels/TakePortraitPhotoPanel';
jest.mock('@edx/frontend-platform/analytics', () => ({
sendTrackEvent: jest.fn(),
}));
jest.mock('../../Camera');
const history = createMemoryHistory();
const IntlTakePortraitPhotoPanel = injectIntl(TakePortraitPhotoPanel);
describe('TakePortraitPhotoPanel', () => {
const defaultProps = {
intl: {},
};
const contextValue = {
facePhotoFile: null,
setFacePhotoFile: jest.fn(),
};
afterEach(() => {
cleanup();
});
it('doesn\'t show next button before photo is taken', async () => {
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlTakePortraitPhotoPanel {...defaultProps} />
</IdVerificationContext.Provider>
</IntlProvider>
</Router>
)));
const button = await screen.findByTestId('next-button');
expect(button).not.toBeVisible();
});
it('shows next button after photo is taken and routes to IdContextPanel', async () => {
contextValue.facePhotoFile = 'test.jpg';
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlTakePortraitPhotoPanel {...defaultProps} />
</IdVerificationContext.Provider>
</IntlProvider>
</Router>
)));
const button = await screen.findByTestId('next-button');
expect(button).toBeVisible();
fireEvent.click(button);
expect(history.location.pathname).toEqual('/id-context');
});
it('routes back to SummaryPanel if that was the source', async () => {
contextValue.facePhotoFile = 'test.jpg';
history.location.state = { fromSummary: true };
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlTakePortraitPhotoPanel {...defaultProps} />
</IdVerificationContext.Provider>
</IntlProvider>
</Router>
)));
const button = await screen.findByTestId('next-button');
fireEvent.click(button);
expect(history.location.pathname).toEqual('/summary');
});
});

View File

@@ -1,21 +0,0 @@
const glob = require('glob');
const PurgecssPlugin = require('purgecss-webpack-plugin');
const { createConfig } = require('@edx/frontend-build');
module.exports = createConfig('webpack-prod', {
plugins: [
// Scan files for class names and ids and remove unused css
new PurgecssPlugin({
paths: [].concat(
// Scan files in this app
glob.sync('src/**/*', { nodir: true }),
// Scan files in any edx frontend-component
glob.sync('node_modules/@edx/frontend-component*/**/*', { nodir: true }),
// Scan files in paragon
glob.sync('node_modules/@edx/paragon/**/*', { nodir: true }),
),
// Protect react-css-transition class names
whitelistPatterns: [/-enter/, /-appear/, /-exit/],
}),
],
});