Compare commits

...

310 Commits

Author SHA1 Message Date
Ahtisham Shahid
fd02f8b3f4 Merge pull request #1571 from openedx/aansari/INF-2127
feat: added captcha v3 on registration
2025-09-17 20:00:47 +05:00
Awais Ansari
5ba6adcd7d refactor: fixed lint issue 2025-09-17 16:52:46 +05:00
Awais Ansari
527bc5aa37 test: added captcha hook test cases 2025-09-17 16:49:01 +05:00
Awais Ansari
fd5122bc10 refactor: improve code and test coverage 2025-09-17 16:03:15 +05:00
Awais Ansari
97c9a09efa feat: added captcha v3 on registration 2025-09-16 16:59:03 +05:00
Awais Ansari
cf3e8b5c7f Merge pull request #1569 from openedx/aansari/sync-with-2u-main-branch-demo
chore: sync 2u-main with master
2025-09-15 17:34:59 +05:00
Awais Ansari
764d5f51e1 test: resolved test conflicts 2025-09-15 17:22:19 +05:00
Awais Ansari
c4ef3dbbd9 Merge branch 'master' of github.com:openedx/frontend-app-authn into aansari/sync-with-2u-main-branch-demo 2025-09-15 17:14:56 +05:00
renovate[bot]
f8a5cb50ed fix(deps): update dependency @edx/frontend-platform to v8.5.1 (#1568)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-15 02:15:50 +00:00
Samuel Allan
b97f777b6f fix: update frontend-build to fix install issues (#1553)
Earlier versions of @openedx/frontend-build used on older version of
'sharp', which caused intermittent installation issues. The version of
'sharp' was updated in @openedx/frontend-build to fix these issues, so
the frontend-build version can be updated here, to fix the issues in
this project too. See
https://github.com/openedx/frontend-build/issues/664 and
https://github.com/openedx/frontend-build/pull/665 for more information.

The frontend-build dependency was updated by:

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

Private-ref: https://tasks.opencraft.com/browse/BB-9953
2025-09-08 13:39:50 -06:00
renovate[bot]
b2f7579054 fix(deps): update dependency form-urlencoded to v6.1.6 (#1560)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-04 08:36:57 +00:00
renovate[bot]
24742c1cf5 chore(deps): update dependency jest to v30.1.3 (#1557)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-02 16:30:15 +00:00
renovate[bot]
051383e68a chore(deps): update dependency jest to v30.1.2 (#1555)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-01 08:23:58 +00:00
renovate[bot]
5443ebd01b chore(deps): update dependency @openedx/frontend-build to v14.6.2 (#1554)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-01 07:55:50 +00:00
renovate[bot]
3aa2422735 chore(deps): update dependency jest to v30.1.1 (#1552)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-27 20:30:04 +00:00
renovate[bot]
90a7dfeb15 chore(deps): update dependency jest to v30.1.0 (#1551)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-27 05:27:12 +00:00
Hassan Raza
a552d025b6 chore: Handle forbidden username exceptions on registration (#1545) (#1550) 2025-08-22 18:43:06 +05:00
renovate[bot]
97c7bd744f fix(deps): update dependency @fortawesome/react-fontawesome to v0.2.6 (#1549)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-22 02:27:19 +00:00
renovate[bot]
55c5f705fb fix(deps): update dependency @fortawesome/react-fontawesome to v0.2.5 (#1548)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-20 03:30:08 +00:00
renovate[bot]
f4e2adc261 fix(deps): update dependency @openedx/paragon to v23.14.2 (#1546)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-13 16:49:41 +00:00
Hassan Raza
58ec90aca6 chore: Handle forbidden username exceptions on registration (#1545) 2025-08-13 07:35:30 +00:00
Diana Villalvazo
76e400f0ad refactor: Replace of injectIntl with useIntl (#1540) 2025-08-12 11:00:33 -04:00
Diana Villalvazo
5bd6926f2f refactor: Replace of injectIntl with useIntl (#1539) 2025-08-12 10:46:01 -04:00
Diana Villalvazo
43a584ebd1 refactor: Replace of injectIntl with useIntl (#1538) 2025-08-12 09:42:57 -04:00
sundasnoreen12
4cf0a64d81 Merge pull request #1541 from WGU-Open-edX/1526/injectIntl-4of4
Replace of injectIntl with useIntl() 4/4
2025-08-11 12:31:50 +05:00
diana-villalvazo-wgu
db3d007c51 refactor: Replace of injectIntl with useIntl 2025-08-07 10:50:11 -05:00
renovate[bot]
55a930840f fix(deps): update dependency @edx/frontend-platform to v8.5.0 (#1543)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-06 21:25:05 +00:00
renovate[bot]
fad82b52ad fix(deps): update dependency @openedx/paragon to v23.14.1 (#1537)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-04 14:50:25 +00:00
renovate[bot]
41450686aa chore(deps): update dependency ts-jest to v29.4.1 (#1536)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-04 02:11:37 +00:00
Kyle McCormick
2fcda640f5 chore: Delete CODEOWNERS (#1535)
See: https://github.com/openedx/axim-engineering/issues/1511
2025-07-31 16:18:27 -04:00
renovate[bot]
82252f9a7c fix(deps): update dependency @fortawesome/react-fontawesome to v0.2.3 (#1531)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-24 15:03:04 +00:00
renovate[bot]
818d0278a5 chore(deps): update dependency jest to v30.0.5 (#1527)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-22 08:44:21 +00:00
Awais Ansari
2cb62ca6d4 chore: sync 2u-main with master (#1520)
* fix(deps): update dependency @edx/frontend-platform to v8.3.2 (#1454)

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

* chore(deps): update dependency babel-plugin-formatjs to v10.5.36 (#1455)

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

* fix(deps): update dependency @edx/frontend-platform to v8.3.3 (#1456)

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

* fix(deps): update dependency algoliasearch-helper to v3.24.3 (#1457)

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

* chore(deps): update dependency @openedx/frontend-build to v14.3.3 (#1459)

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

* fix(deps): update dependency @openedx/paragon to v22.16.1 (#1460)

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

* chore(deps): update `@openedx` dependencies to versions that support React 18 (#1458)

* docs: Update migrated edx.rtd links to docs.openedx.org

* docs: Add instructions on using Tutor for development

* chore(deps): update dependency babel-plugin-formatjs to v10.5.37 (#1461)

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

* fix(deps): update dependency @openedx/paragon to v22.16.2 (#1462)

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

* chore(deps): update dependency @openedx/frontend-build to v14.4.1 (#1464)

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

* fix: properly set background color for floating labels (#1468)

* fix(deps): update dependency @edx/frontend-platform to v8.3.4 (#1471)

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

* feat: upgrade to react 18 (#1466)

* chore(deps): update dependency @openedx/frontend-build to v14.5.0 (#1474)

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

* chore: remove husky 🪓🐶

We remove husky, which is triggering pre-push git hooks, including
running "npm lint". This is causing failures when building Docker
images, because "npm clean-install --omit=dev" automatically triggers "npm
prepare", which attemps to run "husky". But husky is not listed in the
build dependencies, only in devDependencies. As a consequence, package
installation is failing with the following error:

        14.13 > @edx/frontend-app-ora-grading@0.0.1 prepare
        14.13 > husky install
        14.13
        14.15 sh: 1: husky: not found

Similar to: https://github.com/openedx/frontend-app-learning/pull/1622

* chore(deps): update dependency @openedx/frontend-build to v14.6.0 (#1477)

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

* fix(deps): update dependency algoliasearch-helper to v3.25.0 (#1478)

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

* fix(deps): update dependency core-js to v3.42.0 (#1479)

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

* chore(deps): update dependency babel-plugin-formatjs to v10.5.38 (#1480)

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

* fix(deps): update dependency @edx/frontend-platform to v8.3.5 (#1481)

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

* fix(deps): update dependency @edx/frontend-platform to v8.3.6 (#1482)

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

* fix(deps): update dependency @edx/frontend-platform to v8.3.7 (#1484)

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

* fix(deps): update react-router monorepo to v6.30.1 (#1486)

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

* fix(deps): update dependency @openedx/paragon to v22.18.0 (#1487)

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

* fix(deps): update dependency @openedx/paragon to v22.18.1 (#1488)

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

* fix(deps): update dependency @edx/frontend-platform to v8.3.8 (#1489)

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

* fix(deps): update dependency @openedx/paragon to v22.18.2 (#1490)

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

* fix(deps): update dependency @openedx/paragon to v22.20.0 (#1491)

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

* fix(deps): update dependency @edx/frontend-platform to v8.3.9 (#1492)

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

* fix(deps): update dependency core-js to v3.43.0 (#1493)

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

* fix(deps): update dependency @edx/frontend-platform to v8.4.0 (#1494)

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

* chore(deps): update dependency jest to v30 (#1495)

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

* fix(deps): update dependency @openedx/paragon to v22.20.1 (#1496)

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

* chore(deps): update dependency babel-plugin-formatjs to v10.5.39 (#1499)

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

* fix(deps): update dependency @openedx/paragon to v22.20.2 (#1500)

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

* fix(deps): update dependency algoliasearch-helper to v3.26.0 (#1501)

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

* chore(deps): update dependency @openedx/frontend-build to v14.6.1 (#1503)

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

* feat!: add design tokens support (#1504)

BREAKING CHANGE: Pre-design-tokens theming is no longer supported.

Co-authored-by: Diana Olarte <diana.olarte@edunext.co>

* chore(deps): update dependency jest to v30.0.1 (#1506)

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

* chore(deps): update dependency jest to v30.0.2 (#1507)

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

* fix(deps): update dependency algoliasearch to v4.25.0 (#1508)

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

* chore(deps): update dependency eslint-plugin-import to v2.32.0 (#1509)

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

* fix(deps): update dependency algoliasearch to v4.25.2 (#1510)

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

* chore(deps): update dependency jest to v30.0.3 (#1513)

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

* fix(deps): update dependency @openedx/paragon to v23.13.0 (#1514)

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

* chore(deps): update dependency jest to v30.0.4 (#1516)

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

* fix(deps): update dependency @openedx/paragon to v23.14.0 (#1517)

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

* fix: component destory issue after react 18 upgrade

* fix: fixed lint issue

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Brian Smith <112954497+brian-smith-tcril@users.noreply.github.com>
Co-authored-by: sarina <sarina@axim.org>
Co-authored-by: Régis Behmo <regis@behmo.com>
Co-authored-by: Adolfo R. Brandes <adolfo@axim.org>
Co-authored-by: Diana Olarte <diana.olarte@edunext.co>
Co-authored-by: eemaanamir <eemaan.amir@gmail.com>
2025-07-09 12:05:30 +05:00
renovate[bot]
ff3fce99db fix(deps): update dependency @openedx/paragon to v23.14.0 (#1517)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-03 19:01:47 +00:00
renovate[bot]
157c302384 chore(deps): update dependency jest to v30.0.4 (#1516)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-03 00:16:29 +00:00
renovate[bot]
f2a905d373 fix(deps): update dependency @openedx/paragon to v23.13.0 (#1514)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-26 23:59:51 +00:00
renovate[bot]
e984a0b07b chore(deps): update dependency jest to v30.0.3 (#1513)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-25 03:34:38 +00:00
renovate[bot]
7150d4562a fix(deps): update dependency algoliasearch to v4.25.2 (#1510)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-23 17:06:34 +00:00
renovate[bot]
451056866f chore(deps): update dependency eslint-plugin-import to v2.32.0 (#1509)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-20 23:30:16 +00:00
renovate[bot]
76f0cc54d9 fix(deps): update dependency algoliasearch to v4.25.0 (#1508)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-19 19:51:42 +00:00
renovate[bot]
fb70f7a1c2 chore(deps): update dependency jest to v30.0.2 (#1507)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-19 15:46:32 +00:00
renovate[bot]
b664150b4d chore(deps): update dependency jest to v30.0.1 (#1506)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-18 22:50:33 +00:00
Brian Smith
da5a2e31b6 feat!: add design tokens support (#1504)
BREAKING CHANGE: Pre-design-tokens theming is no longer supported.

Co-authored-by: Diana Olarte <diana.olarte@edunext.co>
2025-06-18 14:29:11 -04:00
renovate[bot]
486d0bfd37 chore(deps): update dependency @openedx/frontend-build to v14.6.1 (#1503)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-18 03:45:28 +00:00
renovate[bot]
9332fc113a fix(deps): update dependency algoliasearch-helper to v3.26.0 (#1501)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-16 17:05:31 +00:00
renovate[bot]
181e837ca4 fix(deps): update dependency @openedx/paragon to v22.20.2 (#1500)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-16 06:13:56 +00:00
renovate[bot]
735a9afc3c chore(deps): update dependency babel-plugin-formatjs to v10.5.39 (#1499)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-16 00:51:57 +00:00
renovate[bot]
319c48f1c8 fix(deps): update dependency @openedx/paragon to v22.20.1 (#1496)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-12 17:38:03 +00:00
renovate[bot]
fbd73bfbfe chore(deps): update dependency jest to v30 (#1495)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-10 05:28:53 +00:00
renovate[bot]
27a63cf406 fix(deps): update dependency @edx/frontend-platform to v8.4.0 (#1494)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-09 16:33:27 +00:00
renovate[bot]
7ea351f6a0 fix(deps): update dependency core-js to v3.43.0 (#1493)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-09 07:34:11 +00:00
renovate[bot]
61e8c254d7 fix(deps): update dependency @edx/frontend-platform to v8.3.9 (#1492)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-06 19:27:33 +00:00
renovate[bot]
3a08e790c3 fix(deps): update dependency @openedx/paragon to v22.20.0 (#1491)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-04 18:02:08 +00:00
renovate[bot]
b4c5171886 fix(deps): update dependency @openedx/paragon to v22.18.2 (#1490)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-03 20:32:20 +00:00
renovate[bot]
7b83c416f8 fix(deps): update dependency @edx/frontend-platform to v8.3.8 (#1489)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-30 21:59:07 +00:00
renovate[bot]
a3c261bb13 fix(deps): update dependency @openedx/paragon to v22.18.1 (#1488)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-22 18:12:22 +00:00
renovate[bot]
98d03aa29f fix(deps): update dependency @openedx/paragon to v22.18.0 (#1487)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-21 22:11:58 +00:00
renovate[bot]
f5d5e2fd02 fix(deps): update react-router monorepo to v6.30.1 (#1486)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-20 20:32:18 +00:00
renovate[bot]
490bf27ed1 fix(deps): update dependency @edx/frontend-platform to v8.3.7 (#1484)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-19 13:30:11 +00:00
renovate[bot]
780acac2fd fix(deps): update dependency @edx/frontend-platform to v8.3.6 (#1482)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-12 11:17:03 +00:00
renovate[bot]
2ea763701d fix(deps): update dependency @edx/frontend-platform to v8.3.5 (#1481)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-05 11:51:58 +00:00
renovate[bot]
e2d9ba5857 chore(deps): update dependency babel-plugin-formatjs to v10.5.38 (#1480)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-05 07:15:18 +00:00
renovate[bot]
747d656f0a fix(deps): update dependency core-js to v3.42.0 (#1479)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-29 19:33:32 +00:00
renovate[bot]
8638ed5cf4 fix(deps): update dependency algoliasearch-helper to v3.25.0 (#1478)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-29 15:06:11 +00:00
renovate[bot]
ca2e7f554a chore(deps): update dependency @openedx/frontend-build to v14.6.0 (#1477)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-28 23:54:00 +00:00
ayesha waris
f3f14fb3e7 fix: fixed cohesion page url setup (#1476)
Co-authored-by: Ayesha Waris <ayesha.waris@A006-01000.local>
2025-04-23 20:25:22 +05:00
ayesha waris
6c8b3835b6 fix: fixed cohesion pageview (#1475)
Co-authored-by: Ayesha Waris <ayesha.waris@192.168.10.17>
2025-04-23 13:17:14 +05:00
Adolfo R. Brandes
0e94124d74 Merge pull request #1470 from regisb/regisb/no-husky
chore: remove husky 🪓🐶
2025-04-14 14:12:48 -03:00
Régis Behmo
af7edd8a3f chore: remove husky 🪓🐶
We remove husky, which is triggering pre-push git hooks, including
running "npm lint". This is causing failures when building Docker
images, because "npm clean-install --omit=dev" automatically triggers "npm
prepare", which attemps to run "husky". But husky is not listed in the
build dependencies, only in devDependencies. As a consequence, package
installation is failing with the following error:

        14.13 > @edx/frontend-app-ora-grading@0.0.1 prepare
        14.13 > husky install
        14.13
        14.15 sh: 1: husky: not found

Similar to: https://github.com/openedx/frontend-app-learning/pull/1622
2025-04-14 18:53:56 +02:00
renovate[bot]
9323f119c8 chore(deps): update dependency @openedx/frontend-build to v14.5.0 (#1474)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-11 18:08:35 +00:00
Brian Smith
38a1924c6a feat: upgrade to react 18 (#1466) 2025-04-04 10:17:53 -04:00
sundasnoreen12
8cbe6ce02e Merge pull request #1472 from openedx/sundas/upgrade-frontend-build
fix: properly set background color for floating labels
2025-04-03 17:05:24 +05:00
sundasnoreen12
a53334d3bf fix: properly set background color for floating labels 2025-04-03 16:48:53 +05:00
renovate[bot]
2d7303009f fix(deps): update dependency @edx/frontend-platform to v8.3.4 (#1471)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-02 18:46:51 +00:00
Brian Smith
dfcb94a831 fix: properly set background color for floating labels (#1468) 2025-04-01 12:51:38 -04:00
renovate[bot]
520dd6ed6b chore(deps): update dependency @openedx/frontend-build to v14.4.1 (#1464)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-28 14:49:39 +00:00
sundasnoreen12
0172c79dd9 Merge pull request #1465 from openedx/sundas/paragon-upgrade
feat: upgraded package for paragon
2025-03-28 14:17:25 +05:00
sundasnoreen12
5d4abcbab3 feat: upgraded package for paragon 2025-03-28 13:16:34 +05:00
renovate[bot]
1b32dbfa19 fix(deps): update dependency @openedx/paragon to v22.16.2 (#1462)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-26 12:28:50 +00:00
renovate[bot]
f7d9bdb5b5 chore(deps): update dependency babel-plugin-formatjs to v10.5.37 (#1461)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-26 06:50:31 +00:00
sarina
063ec80cde docs: Add instructions on using Tutor for development 2025-03-25 14:40:55 -03:00
sarina
3cbb134c3a docs: Update migrated edx.rtd links to docs.openedx.org 2025-03-25 14:40:55 -03:00
Brian Smith
059a60d1c8 chore(deps): update @openedx dependencies to versions that support React 18 (#1458) 2025-03-25 12:22:43 -04:00
renovate[bot]
c88d701271 fix(deps): update dependency @openedx/paragon to v22.16.1 (#1460)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-24 22:21:52 +00:00
renovate[bot]
33b98b356b chore(deps): update dependency @openedx/frontend-build to v14.3.3 (#1459)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-24 20:18:14 +00:00
renovate[bot]
1f81699af4 fix(deps): update dependency algoliasearch-helper to v3.24.3 (#1457)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-24 13:55:06 +00:00
renovate[bot]
13aa77fc70 fix(deps): update dependency @edx/frontend-platform to v8.3.3 (#1456)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-24 07:13:09 +00:00
renovate[bot]
66531831b7 chore(deps): update dependency babel-plugin-formatjs to v10.5.36 (#1455)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-24 03:42:16 +00:00
renovate[bot]
846d3f0662 fix(deps): update dependency @edx/frontend-platform to v8.3.2 (#1454)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-21 19:54:09 +00:00
sundasnoreen12
c9987eb2f4 Merge pull request #1425 from openedx/sundas-new/INF-1779
feat: implemented restricted country implementation
2025-03-20 12:59:23 +05:00
sundasnoreen12
a2ad9d5248 refactor: optimize code 2025-03-20 12:52:58 +05:00
sundasnoreen12
cca87bd16a fix: fixed issue by removing empty array 2025-03-20 12:52:50 +05:00
sundasnoreen12
206c4c887b test: refactor test file 2025-03-20 12:52:50 +05:00
sundasnoreen12
09dc21eb0e feat: implemented restricted country implementation 2025-03-20 12:52:50 +05:00
ayesha waris
945af2bdfd fix: fixed tagular console error if cohesion script is not loaded (#1452)
Co-authored-by: Ayesha Waris <ayesha.waris@192.168.10.3>
2025-03-19 23:31:59 +05:00
Awais Ansari
0dca6f3fdc Merge pull request #1451 from openedx/aansari/sync-with-master
chore: sync with master
2025-03-18 13:11:41 +05:00
Awais Ansari
189a67c9dc Merge branch 'master' of github.com:openedx/frontend-app-authn into aansari/sync-with-master 2025-03-18 12:58:16 +05:00
sundasnoreen12
3dceb63b9c Merge pull request #1445 from openedx/sundas/INF-1819
fix: blocked web crawlers for auth MFE
2025-03-10 12:25:26 +05:00
renovate[bot]
f78e84ee0a fix(deps): update dependency @edx/frontend-platform to v8.3.1 (#1449)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-07 23:16:01 +00:00
ayesha waris
9385174b93 fix: adding defer to cohesion script (#1447)
Co-authored-by: Ayesha Waris <ayesha.waris@192.168.1.11>
2025-03-06 13:43:09 +05:00
renovate[bot]
2d27da2391 fix(deps): update dependency @openedx/paragon to v22.16.0 (#1446)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-05 18:03:14 +00:00
sundasnoreen12
86ed8e2361 fix: blocked web crawlers for auth MFE 2025-03-05 15:27:09 +05:00
ayesha waris
f5a6ece6b1 refactor: moved updated script to index.html (#1443)
Co-authored-by: Ayesha Waris <ayesha.waris@192.168.1.11>
2025-03-05 13:44:49 +05:00
renovate[bot]
78413be34a chore(deps): update dependency @openedx/frontend-build to v14.3.2 (#1444)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-05 01:30:17 +00:00
renovate[bot]
88866a39c1 fix(deps): update dependency algoliasearch-helper to v3.24.2 (#1442)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-04 16:02:35 +00:00
ayesha waris
0d71e31ffb feat: update cohesion script (#1441)
Co-authored-by: Ayesha Waris <ayesha.waris@192.168.1.11>
2025-03-04 14:47:29 +05:00
renovate[bot]
dc9699c033 fix(deps): update dependency @edx/frontend-platform to v8.3.0 (#1440)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-04 02:44:20 +00:00
renovate[bot]
00a0e27062 fix(deps): update dependency core-js to v3.41.0 (#1439)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-03 03:32:41 +00:00
Awais Ansari
38dd2944b8 Revert "feat: updated cohesion snippet (#1433)" (#1437)
This reverts commit 244b9e68e6.
2025-02-28 16:01:29 +05:00
Awais Ansari
f4708ed274 Revert "fix: removed style tag for updated cohesion script (#1434)" (#1438)
This reverts commit cb7300441c.
2025-02-28 16:01:16 +05:00
renovate[bot]
6839afcf3c fix(deps): update react-router monorepo to v6.30.0 (#1435)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-27 18:41:40 +00:00
ayesha waris
cb7300441c fix: removed style tag for updated cohesion script (#1434)
Co-authored-by: Ayesha Waris <ayesha.waris@A006-01000.local>
2025-02-27 18:39:55 +05:00
ayesha waris
244b9e68e6 feat: updated cohesion snippet (#1433)
Co-authored-by: Ayesha Waris <ayesha.waris@192.168.10.37>
2025-02-27 16:34:56 +05:00
renovate[bot]
1cd9c58c1a fix(deps): update dependency @openedx/paragon to v22.15.3 (#1432)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-25 22:14:39 +00:00
renovate[bot]
5d481a93c7 fix(deps): update dependency @edx/frontend-platform to v8.2.1 (#1429)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-21 22:56:35 +00:00
renovate[bot]
438d1fcfa7 fix(deps): update dependency @edx/frontend-platform to v8.2.0 (#1428)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-21 03:16:30 +00:00
renovate[bot]
bc912ce139 chore(deps): update dependency @openedx/frontend-build to v14.3.1 (#1427)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-20 23:02:47 +00:00
renovate[bot]
ab1c2d5379 fix(deps): update dependency @openedx/paragon to v22.15.2 (#1426)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-20 15:27:23 +00:00
renovate[bot]
c109f6e771 chore(deps): update dependency babel-plugin-formatjs to v10.5.35 (#1421)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-10 01:03:12 +00:00
renovate[bot]
8976647190 fix(deps): update dependency @openedx/paragon to v22.15.1 (#1420)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-31 21:51:26 +00:00
renovate[bot]
cb051a83ad fix(deps): update dependency @openedx/paragon to v22.15.0 (#1419)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-31 17:40:38 +00:00
renovate[bot]
1b8aec5709 fix(deps): update react-router monorepo to v6.29.0 (#1417)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-30 17:54:09 +00:00
renovate[bot]
9a68e95fcc fix(deps): update dependency algoliasearch-helper to v3.24.1 (#1414)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-28 12:31:35 +00:00
renovate[bot]
c90980afb0 fix(deps): update dependency @openedx/paragon to v22.14.0 (#1413)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-22 22:06:09 +00:00
renovate[bot]
abb8ae5085 fix(deps): update dependency algoliasearch-helper to v3.23.1 (#1412)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-21 18:52:31 +00:00
renovate[bot]
8bb7462098 chore(deps): update dependency babel-plugin-formatjs to v10.5.34 (#1411)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-20 17:48:45 +00:00
renovate[bot]
b4a5397ba1 chore(deps): update dependency babel-plugin-formatjs to v10.5.33 (#1410)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-20 05:32:02 +00:00
Feanil Patel
a43c620dc4 Merge pull request #1406 from salman2013/salman/update-catalog-info-file
Update catalog-info file for release data
2025-01-17 10:42:49 -05:00
salman2013
93d11b8485 chore: Update catalog-info file for release data and remove openedx.yaml 2025-01-17 10:39:31 -05:00
renovate[bot]
68e13d4daf chore(deps): update dependency babel-plugin-formatjs to v10.5.31 (#1409)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-17 09:05:56 +00:00
renovate[bot]
f6617935e3 fix(deps): update react-router monorepo to v6.28.2 (#1408)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-16 18:07:00 +00:00
renovate[bot]
5f4591c046 chore(deps): update dependency @edx/browserslist-config to v1.5.0 (#1407)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-15 16:22:56 +00:00
renovate[bot]
ae52a8cb65 fix(deps): update dependency algoliasearch-helper to v3.23.0 (#1405)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-14 18:38:26 +00:00
renovate[bot]
b8df66ad23 fix(deps): update dependency core-js to v3.40.0 (#1404)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-07 20:46:54 +00:00
renovate[bot]
923776ab96 fix(deps): update dependency @edx/frontend-platform to v8.1.5 (#1403)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-06 12:10:02 +00:00
renovate[bot]
f170f5e3f0 chore(deps): update dependency babel-plugin-formatjs to v10.5.30 (#1402)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-02 18:14:00 +00:00
renovate[bot]
730875ceb2 fix(deps): update dependency @edx/frontend-platform to v8.1.4 (#1401)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-30 09:15:59 +00:00
Awais Ansari
c04ed9aa43 feat: updated cohesion script (#1400) 2024-12-23 21:18:01 +05:00
renovate[bot]
812350d24a fix(deps): update react-router monorepo to v6.28.1 (#1399)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-20 21:25:00 +00:00
renovate[bot]
6879bacb89 fix(deps): update dependency @openedx/paragon to v22.13.0 (#1398)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-18 17:20:36 +00:00
renovate[bot]
9b2b0f2019 fix(deps): update font awesome to v6.7.2 (#1397)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-16 22:17:55 +00:00
renovate[bot]
87884f2d91 fix(deps): update dependency @openedx/paragon to v22.12.0 (#1395)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-16 01:22:07 +00:00
renovate[bot]
3e20fcae57 fix(deps): update dependency @openedx/paragon to v22.11.2 (#1391)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-10 01:57:22 +00:00
renovate[bot]
173896811d chore(deps): update dependency @edx/browserslist-config to v1.4.0 (#1394)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-09 23:47:31 +00:00
renovate[bot]
7af4a08bd9 fix(deps): update dependency algoliasearch-helper to v3.22.6 (#1393)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-09 18:57:08 +00:00
renovate[bot]
6c12b3b034 fix(deps): update dependency @edx/frontend-platform to v8.1.3 (#1392)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-09 15:21:14 +00:00
renovate[bot]
5304085cd8 chore(deps): update dependency babel-plugin-formatjs to v10.5.29 (#1389)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-09 09:02:19 +00:00
renovate[bot]
3cd9ae130c chore(deps): update dependency @openedx/frontend-build to v14.2.2 (#1390)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-09 05:41:46 +00:00
renovate[bot]
28ad2c2cf6 chore(deps): update dependency @openedx/frontend-build to v14.2.1 (#1388)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-09 00:46:21 +00:00
renovate[bot]
3e889df109 chore(deps): update dependency @edx/browserslist-config to v1.3.0 (#1386)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-06 02:36:06 +00:00
ayesha waris
354c73bb2a fix: fixed account activation error message (#1384) 2024-11-29 18:28:23 +05:00
ayesha waris
76a5a5dffa fix: fixed event data (#1383) 2024-11-29 16:30:23 +05:00
Eemaan Amir
5efe9d8344 Merge pull request #1382 from openedx/inf-universal-cookie-update
chore: rebase 2u-main
2024-11-28 12:57:37 +05:00
ayesha waris
cd2003921b fix: fixed duplicate event firing on post SSO login registration (#1381) 2024-11-27 18:56:03 +05:00
Eemaan Amir
52c6efc34d Merge pull request #1357 from openedx/renovate/universal-cookie-7.x
fix(deps): update dependency universal-cookie to v7
2024-11-27 17:14:02 +05:00
Eemaan Amir
7339aec7c2 Merge pull request #1380 from openedx/inf-reselect-update
chore: rebase 2u-main
2024-11-27 14:20:46 +05:00
renovate[bot]
584a84a99c fix(deps): update dependency universal-cookie to v7 2024-11-27 09:07:41 +00:00
Eemaan Amir
7e4ab1c74c Merge pull request #1356 from openedx/renovate/reselect-5.x
fix(deps): update dependency reselect to v5
2024-11-27 14:07:16 +05:00
Eemaan Amir
025870a3b9 Merge pull request #1378 from openedx/inf-checkout-action-update
chore: rebase 2u-main
2024-11-22 13:12:32 +05:00
renovate[bot]
13d89cb3a0 fix(deps): update dependency reselect to v5 2024-11-22 06:55:30 +00:00
Eemaan Amir
5a1e2e6c97 Merge pull request #1147 from openedx/renovate/actions-checkout-4.x
chore(deps): update actions/checkout action to v4
2024-11-22 11:54:42 +05:00
renovate[bot]
6f1cf29a60 chore(deps): update actions/checkout action to v4 2024-11-21 10:15:17 +00:00
Eemaan Amir
8dc77d5db6 Merge pull request #1377 from openedx/inf-github-action-update
chore: rebase 2u-main
2024-11-21 15:14:51 +05:00
renovate[bot]
159f1ae30e fix(deps): update font awesome to v6.7.1 (#1374)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-21 08:56:48 +00:00
Eemaan Amir
e2e626552f Merge pull request #1314 from openedx/repo-tools/salman/add-dependabot-file-e2a206c
chore: enable github action auto update in dependabot.yml
2024-11-21 13:53:25 +05:00
Eemaan Amir
40a1f4ce6b Merge pull request #1373 from openedx/inf-codecov-action-update
chore: rebase 2u-main
2024-11-19 19:18:40 +05:00
edX requirements bot
308d7c62e4 chore: enable github action auto update in dependabot.yml 2024-11-19 19:18:17 +05:00
Eemaan Amir
0bc78da55d Merge pull request #1367 from openedx/renovate/codecov-codecov-action-5.x
chore(deps): update codecov/codecov-action action to v5
2024-11-19 18:28:46 +05:00
Eemaan Amir
bb9fcd91c0 Merge pull request #1372 from openedx/inf-husky-update
chore: rebase 2u-main
2024-11-19 16:40:26 +05:00
renovate[bot]
6527caea54 chore(deps): update codecov/codecov-action action to v5 2024-11-19 10:57:48 +00:00
Eemaan Amir
a52912e35b Merge pull request #1149 from openedx/renovate/husky-9.x
chore(deps): update dependency husky to v9
2024-11-19 15:57:32 +05:00
renovate[bot]
6479382b90 chore(deps): update dependency husky to v9 2024-11-19 00:06:24 +00:00
renovate[bot]
4ce36bb12c fix(deps): update font awesome to v6.7.0 (#1371)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-18 23:17:51 +00:00
renovate[bot]
4cc7723984 chore(deps): update dependency @openedx/frontend-build to v14.2.0 (#1370)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-18 20:24:00 +00:00
renovate[bot]
3c3d359d4e chore(deps): update dependency babel-plugin-formatjs to v10.5.26 (#1369)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-18 09:44:26 +00:00
renovate[bot]
cccbf3a9d1 chore(deps): update dependency babel-plugin-formatjs to v10.5.25 (#1368)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-18 06:28:06 +00:00
ayesha waris
0fa00290da fix: fixed cohesion snippet loading (#1366) 2024-11-13 17:45:20 +05:00
renovate[bot]
4a3fd2ee8e fix(deps): update dependency @openedx/paragon to v22.10.0 (#1365)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-13 02:40:50 +00:00
Eemaan Amir
b18caa2da0 Merge pull request #1363 from openedx/inf-algoliasearch-helper-update
Rebase with 2u-main
2024-11-11 19:02:34 +05:00
Eemaan Amir
48d7cb386a Merge pull request #1362 from openedx/renovate/algolia-instantsearch-monorepo
fix(deps): update dependency algoliasearch-helper to v3.22.5
2024-11-11 18:52:42 +05:00
renovate[bot]
bdf9cab869 fix(deps): update dependency algoliasearch-helper to v3.22.5 2024-11-11 12:41:03 +00:00
Eemaan Amir
be02dabf40 Merge pull request #1361 from openedx/revert-1313-renovate/algolia-instantsearch-monorepo
Revert "fix(deps): update dependency algoliasearch-helper to v3.22.5"
2024-11-11 17:39:58 +05:00
Eemaan Amir
c535fb9d24 Revert "fix(deps): update dependency algoliasearch-helper to v3.22.5"
This reverts commit 8ab8d09b97.
2024-11-11 17:32:28 +05:00
ayesha waris
5ca86f9183 perf: updated cohesion id stitching script (#1358) 2024-11-11 17:17:58 +05:00
renovate[bot]
8ab8d09b97 fix(deps): update dependency algoliasearch-helper to v3.22.5 2024-11-11 16:45:38 +05:00
renovate[bot]
286c70d50f fix(deps): update react-router monorepo to v6.28.0 (#1355)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-08 12:00:01 +00:00
renovate[bot]
8939e5b91f chore(deps): update dependency babel-plugin-formatjs to v10.5.24 2024-11-06 10:42:36 +00:00
renovate[bot]
bc9f7b3bce fix(deps): update react-router monorepo to v6.27.0 2024-11-01 16:32:19 +00:00
renovate[bot]
fd0bcb9e5f fix(deps): update dependency core-js to v3.39.0 2024-11-01 13:58:08 +00:00
renovate[bot]
98e0167ef1 fix(deps): update dependency @openedx/paragon to v22.9.0 2024-11-01 11:23:30 +00:00
renovate[bot]
8091085f45 chore(deps): update dependency eslint-plugin-import to v2.31.0 2024-11-01 06:51:18 +00:00
renovate[bot]
cd5abd1d9c fix(deps): update dependency redux-mock-store to v1.5.5 2024-11-01 03:09:49 +00:00
renovate[bot]
2a88f435b9 fix(deps): update dependency @edx/frontend-platform to v8.1.2 2024-11-01 00:54:11 +00:00
renovate[bot]
fe1e9c5629 chore(deps): update dependency @openedx/frontend-build to v14.1.5 2024-10-31 21:45:56 +00:00
renovate[bot]
0e363ca724 chore(deps): update dependency babel-plugin-formatjs to v10.5.22 2024-10-31 19:54:28 +00:00
Bilal Qamar
c874638bd1 test: Remove support for Node 18 (#1312)
Co-authored-by: Brian Smith <112954497+brian-smith-tcril@users.noreply.github.com>
2024-10-31 15:51:22 -04:00
Brian Smith
e5c3b1ed41 Revert "fix(deps): update dependency @testing-library/react to v16" (#1339)
This reverts commit 8a27b8cc37.
2024-10-31 15:25:11 -04:00
renovate[bot]
8a27b8cc37 fix(deps): update dependency @testing-library/react to v16 2024-10-31 15:09:42 -04:00
ayesha waris
2a9dbe9d30 fix: delaying nudge password redirection (#1337)
* fix: delaying  nudge password redirection

* fix: fixed test cases
2024-10-24 14:41:35 +05:00
ayesha waris
62508e3bc7 fix: fixed event aborting in firefox due to page redirect (#1336)
* fix: fixed event aborting in firefox due to page redirect

* fix: fixed test cases

* refactor: created a util for redirect
2024-10-23 20:00:17 +05:00
ayesha waris
ceb489753b feat: added update cohesion script (#1335) 2024-10-22 15:11:11 +05:00
ayesha waris
5035a07e0a fix: fixed nudge password edge case (#1334) 2024-10-19 00:30:40 +05:00
Awais Ansari
f086a165e2 fix: removed duplicate registration events (#1333) 2024-10-18 20:01:26 +05:00
Awais Ansari
9239df3620 fix: cohesion script and SSO issue (#1332)
* fix: userId error on script load

* fix: SSO cohesion event
2024-10-18 19:26:04 +05:00
Awais Ansari
009125c3ef fix: triggered login event on success (#1331)
* fix: triggered login event on success

* fix: fixed failing test cases

---------

Co-authored-by: ayeshoali <ayeshoali@gmail.com>
2024-10-18 18:28:40 +05:00
ayesha waris
b69ed6e422 fix: fixed opt out event text (#1330) 2024-10-18 14:42:28 +05:00
ayesha waris
07ee2392e9 feat: added cohesion events tracking (#1329)
* feat: added cohesion events tracking

* test: fixed failing test cases

* refactor: moved cohesion code into a folder

* refactor: fire event on successful form submission

---------

Co-authored-by: Awais Ansari <awais.ansari63@gmail.com>
2024-10-17 20:28:25 +05:00
Awais Ansari
2bfce01772 Merge pull request #1328 from openedx/aansari/rebase-with-master
chore: rebase 2u main with master
2024-10-11 16:06:10 +05:00
Awais Ansari
1477ed33d7 Merge branch 'master' of github.com:openedx/frontend-app-authn into aansari/rebase-with-master 2024-10-04 18:13:47 +05:00
Awais Ansari
c4f1a97316 build: updated webpack.prod.config.js (#1327)
* build: updated webpack.prod.config.js

* fix: lint error
2024-09-25 17:16:32 +05:00
Awais Ansari
47b0501e1c feat: add MainAppSlot for chatbot plugin (#1320)
* feat: add MainAppSlot for chatbot plugin

* test: added test for MainAppSlot

* chore: add read me for plugin-slot
2024-09-25 13:58:01 +05:00
renovate[bot]
046fbeab01 fix(deps): update dependency react-loading-skeleton to v3.5.0 2024-09-23 01:48:16 +00:00
renovate[bot]
27ea509989 fix(deps): update dependency @openedx/paragon to v22.8.1 2024-09-20 21:22:27 +00:00
renovate[bot]
27f0508e6e chore(deps): update dependency eslint-plugin-import to v2.30.0 2024-09-20 19:10:37 +00:00
renovate[bot]
c53fedf7a1 chore(deps): update dependency @openedx/frontend-build to v14.1.4 2024-09-20 17:06:05 +00:00
renovate[bot]
0f1a5e9aef fix(deps): update react-router monorepo to v6.26.2 2024-09-20 14:37:28 +00:00
Feanil Patel
6cb4b799b7 Merge pull request #1316 from openedx/feanil/ubuntu_upgrade
build: Switch to ubuntu-latest for builds
2024-09-20 10:32:28 -04:00
Feanil Patel
439b9161b5 build: Switch to ubuntu-latest for builds
This code does not have any dependencies that are specific to any specific
version of ubuntu.  So instead of testing on a specific version and then needing
to do work to keep the versions up-to-date, we switch to the ubuntu-latest
target which should be sufficient for testing purposes.

This work is being done as a part of https://github.com/openedx/platform-roadmap/issues/377

closes https://github.com/openedx/frontend-app-authn/issues/1299
2024-09-20 10:25:36 -04:00
Muhammad Abdullah Waheed
e496bb62c5 Merge pull request #1318 from openedx/manwar/update-2u-main
refactor: sync 2u-main with master
2024-09-19 16:20:26 +05:00
Awais Ansari
b41fca3605 feat: removed Russian Federation from country list (#1315) 2024-09-12 10:01:48 +05:00
Mubbshar Anwar
ac2548913f fix: password reset redirection (#1300)
fix authenticated user redirects to 404 if token is invalide for password reset
VAN-2052
2024-09-12 10:01:48 +05:00
Blue
cd9b3bd084 fix: covert totalRegistrationTime to snake case (#1302)
Description:
Convert totalRegistrationTime to snake case
VAN-1816

Co-authored-by: Ahtesham Quraish <ahtesham.quraish@192.168.1.4>
2024-09-12 10:01:40 +05:00
Syed Sajjad Hussain Shah
efc07aac67 fix: fix datadog js errors (#1296) 2024-09-12 09:59:52 +05:00
Syed Sajjad Hussain Shah
2d50ed224f fix: retain query params in authenticated user redirection (#1288) 2024-09-12 09:59:52 +05:00
Mubbshar Anwar
d10f9b932b fix: fix marketingEmailsOptIn null value (#1294)
Fix marketingEmailsOptIn null value issue for SSO flow on onboarding component

VAN-2013
2024-09-12 09:59:52 +05:00
Mubbshar Anwar
05aa85a5fb fix: remove cookie (#1286)
-remove marketingEmailsOptIn cookie on successful registration
- fix tests
2024-09-12 09:59:52 +05:00
Syed Sajjad Hussain Shah
56bd6d835e fix: set marketing opt in in cookie for sso (#1285) 2024-09-12 09:59:52 +05:00
Muhammad Abdullah Waheed
afd4d24360 feat: added app name identifier in segment events (#1277)
* feat: added app name identifier in registration call

* feat: added utils for tracking events

* refactor: mapped login events

* refactor: mapped forgot password events

* refactor: mapped reset password events

* refactor: mapped register events

* fix: fixed unit tests

* refactor: mapped progressive prifiling events

* fix: fixed unit tests

* refactor: added app name in logistration events

* refactor: resolved PR reviews and fixed tests
2024-09-12 09:59:47 +05:00
Mubbshar Anwar
4898864416 feat: hard code fields on frontend (#1256)
* feat: hard code fields
hard code configurable fields on frontend which includes country field on register page & level of education & gender field on progressive profiling

VAN-1971

* fix: fix secondary provider null name issue
2024-09-12 09:56:53 +05:00
Mubbshar Anwar
739f94d624 Update 2u-main with master (#1254)
* feat: Hide preloaders for third party auth providers if they are disabled

* feat: remove username from the registration from (#1201) (#1241)

Co-authored-by: Attiya Ishaque <atiya.ishaq@arbisoft.com>

* fix: add new entry for another US label (#1244)

Add new entry for for another US label which is United States

* feat: implement multi step registration experiment

Rebase 2u main with master (#1228)

* chore(deps): update dependency babel-plugin-formatjs to v10.5.14

* fix(deps): update dependency @edx/frontend-platform to v7.1.3

* fix(deps): update font awesome to v6.5.2

* chore(deps): update dependency @openedx/frontend-build to v13.1.4

* fix(deps): update dependency @openedx/paragon to v22.2.1

* fix(deps): update dependency algoliasearch to v4.23.3

* fix(deps): update dependency algoliasearch-helper to v3.17.0

---------

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

* feat: add multi step registration eventing (#1226)

* feat: implement multi step registration experiment

* feat: add multi step registration eventing

* fix: fix register button width

* fix: fix register button loader for control

* feat: capture marketing lead in experiment events (#1243)

* revert: multistep registration experiment
revert multistep registration experiment changes

VAN-1930

* feat: implement auto generated username experiment (#1248)

* feat: implement auto generated username registration exp

* feat: add page event for reset password (#1253)

Description: Add page event for reset password page
VAN-1929

---------

Co-authored-by: Stanislav Lunyachek <stanislav.lunyachek@raccoongang.com>
Co-authored-by: Syed Sajjad Hussain Shah <52817156+syedsajjadkazmii@users.noreply.github.com>
Co-authored-by: Attiya Ishaque <atiya.ishaq@arbisoft.com>
Co-authored-by: Blue <ahtesham-quraish@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Syed Sajjad  Hussain Shah <ssajjad@2u.com>
2024-09-12 09:56:53 +05:00
Blue
1819edc9b7 feat: add page event for reset password (#1253)
Description: Add page event for reset password page
VAN-1929
2024-09-12 09:56:53 +05:00
Blue
ad0d75ab0d feat: implement auto generated username experiment (#1248)
* feat: implement auto generated username registration exp
2024-09-12 09:56:53 +05:00
mubbsharanwar
a90ebb7d4d revert: multistep registration experiment
revert multistep registration experiment changes

VAN-1930
2024-09-12 09:52:48 +05:00
Syed Sajjad Hussain Shah
f8290adab5 feat: capture marketing lead in experiment events (#1243) 2024-09-12 09:50:51 +05:00
Syed Sajjad Hussain Shah
788a42b341 fix: fix register button loader for control 2024-09-12 09:49:24 +05:00
Syed Sajjad Hussain Shah
4f48e82959 fix: fix register button width 2024-09-12 09:49:19 +05:00
Syed Sajjad Hussain Shah
99850574fb feat: add multi step registration eventing (#1226)
* feat: implement multi step registration experiment

* feat: add multi step registration eventing
2024-09-12 09:47:16 +05:00
Syed Sajjad Hussain Shah
d66afe98f0 feat: implement multi step registration experiment 2024-09-12 09:44:24 +05:00
Syed Sajjad Hussain Shah
e2cdfce832 Rebase 2u main with master (#1228)
* chore(deps): update dependency babel-plugin-formatjs to v10.5.14

* fix(deps): update dependency @edx/frontend-platform to v7.1.3

* fix(deps): update font awesome to v6.5.2

* chore(deps): update dependency @openedx/frontend-build to v13.1.4

* fix(deps): update dependency @openedx/paragon to v22.2.1

* fix(deps): update dependency algoliasearch to v4.23.3

* fix(deps): update dependency algoliasearch-helper to v3.17.0

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-11 18:23:56 +05:00
Awais Ansari
c1e63da778 feat: removed Russian Federation from country list (#1315) 2024-09-10 21:11:44 +05:00
sundasnoreen12
6ffa45f0c1 Merge pull request #1317 from openedx/sundas/INF-1551
docs: updated catalog-info file for authn MFE
2024-09-10 16:50:23 +05:00
sundasnoreen12
a03ba3e3b3 docs: updated catalog-info file for authn MFE 2024-09-10 13:36:13 +03:00
renovate[bot]
e2a206caa5 fix(deps): update dependency @edx/openedx-atlas to v0.6.2 2024-09-02 10:22:01 +00:00
Bilal Qamar
3a963da819 build: Upgrade to Node 20 (#1295)
* feat: updated node to v20

* refactor: updated package-lock

* refactor: updated package-lock & lockfile version check workflow

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

* refactor: updated lockfile version workflow
2024-09-02 15:18:31 +05:00
Mubbshar Anwar
ecf4c3ae53 fix: password reset redirection (#1300)
fix authenticated user redirects to 404 if token is invalide for password reset
VAN-2052
2024-08-29 09:48:21 +05:00
Blue
4a65f0a84c fix: change the totalRegisterationTime to snake case (#1301)
Description:
Convert the totalRegistrationTime to snake case
VAN-1816

Co-authored-by: Ahtesham Quraish <ahtesham.quraish@192.168.1.4>
2024-08-28 15:08:30 +05:00
Blue
2428b4c389 fix: covert totalRegistrationTime to snake case (#1302)
Description:
Convert totalRegistrationTime to snake case
VAN-1816

Co-authored-by: Ahtesham Quraish <ahtesham.quraish@192.168.1.4>
2024-08-28 14:58:40 +05:00
renovate[bot]
9688bd3699 fix(deps): update react-router monorepo to v6.26.1 2024-08-23 06:35:23 +00:00
renovate[bot]
c123815a55 fix(deps): update dependency core-js to v3.38.1 2024-08-23 05:27:21 +00:00
renovate[bot]
182e669593 chore(deps): update dependency @openedx/frontend-build to v14.1.0 2024-08-23 01:23:22 +00:00
renovate[bot]
65533b8d58 fix(deps): update dependency algoliasearch-helper to v3.22.4 2024-08-22 22:43:06 +00:00
renovate[bot]
45185dba70 fix(deps): update dependency @edx/frontend-platform to v8.1.1 2024-08-22 18:38:57 +00:00
Bilal Qamar
444c4b4434 test: Add Node 20 to CI matrix (#1303) 2024-08-22 14:34:50 -04:00
Syed Sajjad Hussain Shah
099fe8d717 fix: fix datadog js errors (#1296) 2024-08-01 16:06:20 +05:00
Syed Sajjad Hussain Shah
4755540be8 fix: retain query params in authenticated user redirection (#1288) 2024-07-29 11:23:48 +05:00
Mubbshar Anwar
9a30f053c7 fix: fix marketingEmailsOptIn null value (#1294)
Fix marketingEmailsOptIn null value issue for SSO flow on onboarding component

VAN-2013
2024-07-26 15:57:18 +05:00
renovate[bot]
d629d66bf2 fix(deps): update react-router monorepo to v6.25.1 2024-07-22 00:49:21 +00:00
renovate[bot]
9d46d68150 fix(deps): update font awesome to v6.6.0 2024-07-19 23:07:12 +00:00
renovate[bot]
a4ed6a362e fix(deps): update dependency @openedx/paragon to v22.7.0 2024-07-19 18:42:43 +00:00
renovate[bot]
a1a0d3cd96 fix(deps): update dependency algoliasearch-helper to v3.22.3 2024-07-19 15:43:53 +00:00
renovate[bot]
950c401e88 fix(deps): update dependency redux to v4.2.1 2024-07-19 12:37:44 -03:00
Mubbshar Anwar
6b983e18d3 fix: remove cookie (#1286)
-remove marketingEmailsOptIn cookie on successful registration
- fix tests
2024-07-12 17:05:50 +05:00
Syed Sajjad Hussain Shah
327210192c fix: set marketing opt in in cookie for sso (#1285) 2024-07-12 13:18:42 +05:00
renovate[bot]
ce056c9ad2 fix(deps): update react-router monorepo to v6.24.1 2024-07-09 09:47:38 +00:00
renovate[bot]
3bd6e454d0 fix(deps): update dependency @edx/frontend-platform to v8.1.0 2024-07-09 06:59:28 +00:00
renovate[bot]
f52129a11e chore(deps): update dependency iframe-resizer to v4.4.4 2024-07-09 03:58:26 +00:00
renovate[bot]
ea01050163 fix(deps): update dependency algoliasearch-helper to v3.22.2 2024-07-09 00:27:00 +00:00
Muhammad Abdullah Waheed
0d603b5fa1 feat: added app name identifier in segment events (#1277)
* feat: added app name identifier in registration call

* feat: added utils for tracking events

* refactor: mapped login events

* refactor: mapped forgot password events

* refactor: mapped reset password events

* refactor: mapped register events

* fix: fixed unit tests

* refactor: mapped progressive prifiling events

* fix: fixed unit tests

* refactor: added app name in logistration events

* refactor: resolved PR reviews and fixed tests
2024-07-03 17:08:44 +05:00
renovate[bot]
c1ec9b6e99 fix(deps): update dependency algoliasearch-helper to v3.22.1 2024-07-01 18:59:29 +00:00
renovate[bot]
2c509b00ac fix(deps): update dependency @openedx/paragon to v22.6.1 2024-07-01 17:39:58 +00:00
renovate[bot]
ef358fe741 fix(deps): update dependency @edx/frontend-platform to v8.0.4 2024-07-01 12:56:55 +00:00
renovate[bot]
56e0520d9c chore(deps): update dependency @openedx/frontend-build to v14.0.10 2024-07-01 11:26:22 +00:00
Bilal Qamar
1f7b7f5c41 chore: major version upgrades for frontend-platform & frontend-build (#1251) 2024-07-01 13:22:45 +02:00
renovate[bot]
471fa75155 fix(deps): update react-router monorepo to v6.23.1 2024-06-18 22:48:40 +00:00
renovate[bot]
c89d16e529 fix(deps): update dependency core-js to v3.37.1 2024-06-18 20:06:43 +00:00
renovate[bot]
fc02ab820a fix(deps): update dependency algoliasearch-helper to v3.22.0 2024-06-18 17:06:47 +00:00
renovate[bot]
ac23cdcc7a fix(deps): update dependency algoliasearch-helper to v3.21.0 2024-06-18 12:06:40 +00:00
renovate[bot]
02c4c5be29 fix(deps): update dependency @openedx/paragon to v22.6.0 2024-06-18 09:11:03 +00:00
renovate[bot]
3bd7d61e3a fix(deps): update dependency form-urlencoded to v6.1.5 2024-06-18 07:26:49 +00:00
renovate[bot]
32ebc69c0e fix(deps): update dependency @fortawesome/react-fontawesome to v0.2.2 2024-06-18 03:19:51 +00:00
renovate[bot]
c98c3b16c5 fix(deps): update dependency @edx/openedx-atlas to v0.6.1 2024-06-18 02:42:13 +00:00
renovate[bot]
287fe3adfe fix(deps): update dependency @edx/frontend-platform to v7.1.4 2024-06-17 22:30:03 +00:00
renovate[bot]
d4e7b7b371 chore(deps): update dependency iframe-resizer to v4.3.11 2024-06-17 19:04:24 +00:00
renovate[bot]
ad78f068e0 chore(deps): update dependency babel-plugin-formatjs to v10.5.16 2024-06-17 15:05:32 +00:00
Adolfo R. Brandes
d156de2e66 build: Update codecov and use token
Update codecov to the latest version and start using the org-wide token for uploads.

See https://github.com/openedx/wg-frontend/issues/179
2024-06-17 12:02:25 -03:00
Attiya Ishaque
99bca1bd9b fix: frontend validation on email field (#1249) 2024-06-12 14:15:56 +05:00
Mubbshar Anwar
efaa83a1bc feat: hard code fields on frontend (#1256)
* feat: hard code fields
hard code configurable fields on frontend which includes country field on register page & level of education & gender field on progressive profiling

VAN-1971

* fix: fix secondary provider null name issue
2024-06-11 12:01:30 +05:00
Mubbshar Anwar
bd63bb1f15 Update 2u-main with master (#1254)
* feat: Hide preloaders for third party auth providers if they are disabled

* feat: remove username from the registration from (#1201) (#1241)

Co-authored-by: Attiya Ishaque <atiya.ishaq@arbisoft.com>

* fix: add new entry for another US label (#1244)

Add new entry for for another US label which is United States

* feat: implement multi step registration experiment

Rebase 2u main with master (#1228)

* chore(deps): update dependency babel-plugin-formatjs to v10.5.14

* fix(deps): update dependency @edx/frontend-platform to v7.1.3

* fix(deps): update font awesome to v6.5.2

* chore(deps): update dependency @openedx/frontend-build to v13.1.4

* fix(deps): update dependency @openedx/paragon to v22.2.1

* fix(deps): update dependency algoliasearch to v4.23.3

* fix(deps): update dependency algoliasearch-helper to v3.17.0

---------

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

* feat: add multi step registration eventing (#1226)

* feat: implement multi step registration experiment

* feat: add multi step registration eventing

* fix: fix register button width

* fix: fix register button loader for control

* feat: capture marketing lead in experiment events (#1243)

* revert: multistep registration experiment
revert multistep registration experiment changes

VAN-1930

* feat: implement auto generated username experiment (#1248)

* feat: implement auto generated username registration exp

* feat: add page event for reset password (#1253)

Description: Add page event for reset password page
VAN-1929

---------

Co-authored-by: Stanislav Lunyachek <stanislav.lunyachek@raccoongang.com>
Co-authored-by: Syed Sajjad Hussain Shah <52817156+syedsajjadkazmii@users.noreply.github.com>
Co-authored-by: Attiya Ishaque <atiya.ishaq@arbisoft.com>
Co-authored-by: Blue <ahtesham-quraish@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Syed Sajjad  Hussain Shah <ssajjad@2u.com>
2024-06-07 07:57:42 +05:00
Blue
5754c2961a feat: add page event for reset password (#1253)
Description: Add page event for reset password page
VAN-1929
2024-06-04 16:27:14 +05:00
Blue
dcbd644a25 feat: implement auto generated username experiment (#1248)
* feat: implement auto generated username registration exp
2024-05-13 14:11:03 +05:00
Blue
52e438652c 2u-main rebase with master (#1246)
Rebase 2u-main with master
2024-05-07 16:44:47 +05:00
mubbsharanwar
d8947a4c0a revert: multistep registration experiment
revert multistep registration experiment changes

VAN-1930
2024-05-07 11:47:28 +05:00
Blue
8efb22595c fix: add new entry for another US label (#1244)
Add new entry for for another US label which is United States
2024-05-03 10:27:32 +05:00
Syed Sajjad Hussain Shah
73e8913f90 feat: remove username from the registration from (#1201) (#1241)
Co-authored-by: Attiya Ishaque <atiya.ishaq@arbisoft.com>
2024-05-02 08:55:56 +05:00
Syed Sajjad Hussain Shah
03d1666c2c feat: capture marketing lead in experiment events (#1243) 2024-04-25 15:28:05 +05:00
Syed Sajjad Hussain Shah
3782503983 fix: fix register button loader for control 2024-04-22 16:53:23 +05:00
Syed Sajjad Hussain Shah
b219fe3683 fix: fix register button width 2024-04-22 14:37:36 +05:00
Stanislav Lunyachek
3ddaf795f2 feat: Hide preloaders for third party auth providers if they are disabled 2024-04-19 10:55:48 +05:00
Syed Sajjad Hussain Shah
90f650ce3e feat: add multi step registration eventing (#1226)
* feat: implement multi step registration experiment

* feat: add multi step registration eventing
2024-04-18 11:09:32 +05:00
Syed Sajjad Hussain Shah
6f325c20c3 feat: implement multi step registration experiment 2024-04-18 11:09:32 +05:00
Syed Sajjad Hussain Shah
de12dfbf9e Merge pull request #1236 from openedx/master
adding master commits to 2u-main
2024-04-18 10:19:28 +05:00
Syed Sajjad Hussain Shah
c663f6fa30 Rebase 2u main with master (#1228)
* chore(deps): update dependency babel-plugin-formatjs to v10.5.14

* fix(deps): update dependency @edx/frontend-platform to v7.1.3

* fix(deps): update font awesome to v6.5.2

* chore(deps): update dependency @openedx/frontend-build to v13.1.4

* fix(deps): update dependency @openedx/paragon to v22.2.1

* fix(deps): update dependency algoliasearch to v4.23.3

* fix(deps): update dependency algoliasearch-helper to v3.17.0

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-17 17:00:28 +05:00
renovate[bot]
dba93333fd fix(deps): update dependency algoliasearch-helper to v3.17.0 2024-04-17 16:50:35 +05:00
renovate[bot]
611af07326 fix(deps): update dependency algoliasearch to v4.23.3 2024-04-17 16:50:35 +05:00
renovate[bot]
564ec70d9e fix(deps): update dependency @openedx/paragon to v22.2.1 2024-04-17 16:50:35 +05:00
renovate[bot]
65e95a4d1b chore(deps): update dependency @openedx/frontend-build to v13.1.4 2024-04-17 16:50:35 +05:00
renovate[bot]
cf2b50005b fix(deps): update font awesome to v6.5.2 2024-04-17 16:50:35 +05:00
renovate[bot]
faf4ff8488 fix(deps): update dependency @edx/frontend-platform to v7.1.3 2024-04-17 16:50:35 +05:00
renovate[bot]
7d64220852 chore(deps): update dependency babel-plugin-formatjs to v10.5.14 2024-04-17 16:50:35 +05:00
renovate[bot]
a18df02d37 fix(deps): update dependency algoliasearch-helper to v3.17.0 2024-04-11 09:20:07 +00:00
127 changed files with 19623 additions and 13921 deletions

7
.env
View File

@@ -16,6 +16,9 @@ SITE_NAME=null
INFO_EMAIL='' INFO_EMAIL=''
# ***** Cookies ***** # ***** Cookies *****
USER_RETENTION_COOKIE_NAME=null USER_RETENTION_COOKIE_NAME=null
# ***** Cohesion Keys *****
COHESION_WRITE_KEY=''
COHESION_SOURCE_KEY=''
# ***** Links ***** # ***** Links *****
LOGIN_ISSUE_SUPPORT_LINK='' LOGIN_ISSUE_SUPPORT_LINK=''
AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK=null AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK=null
@@ -23,6 +26,7 @@ POST_REGISTRATION_REDIRECT_URL=''
SEARCH_CATALOG_URL='' SEARCH_CATALOG_URL=''
# ***** Features flags ***** # ***** Features flags *****
DISABLE_ENTERPRISE_LOGIN='' DISABLE_ENTERPRISE_LOGIN=''
ENABLE_AUTO_GENERATED_USERNAME=''
ENABLE_DYNAMIC_REGISTRATION_FIELDS='' ENABLE_DYNAMIC_REGISTRATION_FIELDS=''
ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN='' ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN=''
ENABLE_POST_REGISTRATION_RECOMMENDATIONS='' ENABLE_POST_REGISTRATION_RECOMMENDATIONS=''
@@ -40,3 +44,6 @@ BANNER_IMAGE_EXTRA_SMALL=''
# ***** Miscellaneous ***** # ***** Miscellaneous *****
APP_ID='' APP_ID=''
MFE_CONFIG_API_URL='' MFE_CONFIG_API_URL=''
# Fallback in local style files
PARAGON_THEME_URLS={}
RECAPTCHA_SITE_KEY_WEB=''

View File

@@ -25,6 +25,9 @@ ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN='true'
# ***** Cookies ***** # ***** Cookies *****
SESSION_COOKIE_DOMAIN='localhost' SESSION_COOKIE_DOMAIN='localhost'
USER_INFO_COOKIE_NAME='edx-user-info' USER_INFO_COOKIE_NAME='edx-user-info'
# ***** Cohesion Keys *****
COHESION_WRITE_KEY=''
COHESION_SOURCE_KEY=''
# ***** Links ***** # ***** Links *****
LOGIN_ISSUE_SUPPORT_LINK='http://localhost:18000/login-issue-support-url' LOGIN_ISSUE_SUPPORT_LINK='http://localhost:18000/login-issue-support-url'
TOS_AND_HONOR_CODE='http://localhost:18000/honor' TOS_AND_HONOR_CODE='http://localhost:18000/honor'
@@ -41,3 +44,6 @@ APP_ID=''
MFE_CONFIG_API_URL='' MFE_CONFIG_API_URL=''
ZENDESK_KEY='' ZENDESK_KEY=''
ZENDESK_LOGO_URL='' ZENDESK_LOGO_URL=''
# Fallback in local style files
PARAGON_THEME_URLS={}
RECAPTCHA_SITE_KEY_WEB=''

View File

@@ -18,3 +18,7 @@ SEGMENT_KEY=''
SITE_NAME='Your Platform Name Here' SITE_NAME='Your Platform Name Here'
APP_ID='' APP_ID=''
MFE_CONFIG_API_URL='' MFE_CONFIG_API_URL=''
COHESION_WRITE_KEY=''
COHESION_SOURCE_KEY=''
PARAGON_THEME_URLS={}
RECAPTCHA_SITE_KEY_WEB=''

View File

@@ -1,7 +1,7 @@
// eslint-disable-next-line import/no-extraneous-dependencies // eslint-disable-next-line import/no-extraneous-dependencies
const { createConfig } = require('@openedx/frontend-build'); const { createConfig } = require('@openedx/frontend-build');
module.exports = createConfig('eslint', { const config = createConfig('eslint', {
rules: { rules: {
// Temporarily update the 'indent', 'template-curly-spacing' and // Temporarily update the 'indent', 'template-curly-spacing' and
// 'no-multiple-empty-lines' rules since they are causing eslint // 'no-multiple-empty-lines' rules since they are causing eslint
@@ -50,3 +50,14 @@ module.exports = createConfig('eslint', {
'function-paren-newline': 'off', 'function-paren-newline': 'off',
}, },
}); });
config.settings = {
'import/resolver': {
node: {
paths: ['src', 'node_modules'],
extensions: ['.js', '.jsx'],
},
},
};
module.exports = config;

1
.github/CODEOWNERS vendored Normal file
View File

@@ -0,0 +1 @@
* @openedx/2U-infinity

7
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,7 @@
version: 2
updates:
# Adding new check for github-actions
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"

View File

@@ -25,5 +25,5 @@ Include a link to the sandbox for design changes or screenshot for before and af
#### Post-merge Checklist #### Post-merge Checklist
* [ ] Deploy the changes to prod after verifying on stage or ask **@openedx/2u-vanguards** to do it. * [ ] Deploy the changes to prod after verifying on stage or ask **@openedx/2u-infinity** to do it.
* [ ] 🎉 🙌 Celebrate! Thanks for your contribution. * [ ] 🎉 🙌 Celebrate! Thanks for your contribution.

View File

@@ -10,7 +10,7 @@ on:
jobs: jobs:
autoupdate: autoupdate:
name: autoupdate name: autoupdate
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- uses: docker://chinthakagodawita/autoupdate-action:v1 - uses: docker://chinthakagodawita/autoupdate-action:v1
env: env:

View File

@@ -10,17 +10,15 @@ on:
jobs: jobs:
tests: tests:
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Setup Nodejs Env
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
- name: Setup Nodejs - name: Setup Nodejs
uses: actions/setup-node@v3 uses: actions/setup-node@v4
with: with:
node-version: ${{ env.NODE_VER }} node-version-file: '.nvmrc'
- name: Install Dependencies - name: Install Dependencies
run: npm ci run: npm ci
@@ -41,4 +39,7 @@ jobs:
run: npm run build run: npm run build
- name: Run Code Coverage - name: Run Code Coverage
uses: codecov/codecov-action@v3 uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true

View File

@@ -10,4 +10,4 @@ on:
jobs: jobs:
version-check: version-check:
uses: openedx/.github/.github/workflows/lockfile-check.yml@master uses: openedx/.github/.github/workflows/lockfileversion-check-v3.yml@master

2
.nvmrc
View File

@@ -1 +1 @@
18 20

View File

@@ -1,2 +0,0 @@
# The following users are the owners of all frontend-app-authn files
* @openedx/2u-vanguards

View File

@@ -29,7 +29,13 @@ Getting Started
Installation Installation
============ ============
This MFE is bundled with `Devstack <https://github.com/openedx/devstack>`_, see the `Getting Started <https://github.com/openedx/devstack#getting-started>`_ section for setup instructions. `Tutor`_ is currently recommended as a development environment for your new MFE. Please refer to the `relevant tutor-mfe documentation`_ to get started using it.
.. _Tutor: https://github.com/overhangio/tutor
.. _relevant tutor-mfe documentation: https://github.com/overhangio/tutor-mfe?tab=readme-ov-file#mfe-development
Devstack (Deprecated) instructions
==================================
1. Install Devstack using the `Getting Started <https://github.com/openedx/devstack#getting-started>`_ instructions. 1. Install Devstack using the `Getting Started <https://github.com/openedx/devstack#getting-started>`_ instructions.
@@ -51,7 +57,7 @@ This MFE is bundled with `Devstack <https://github.com/openedx/devstack>`_, see
Environment Variables/Setup Notes Environment Variables/Setup Notes
================================= =================================
This MFE is configured via environment variables supplied at build time. All micro-frontends have a shared set of required environment variables, as documented in the Open edX Developer Guide under `Required Environment Variables <https://edx.readthedocs.io/projects/edx-developer-docs/en/latest/developers_guide/micro_frontends_in_open_edx.html#required-environment-variables>`__. This MFE is configured via environment variables supplied at build time. All micro-frontends have a shared set of required environment variables, as documented in the Open edX Developer Guide under `Required Environment Variables <https://github.com/overhangio/tutor-mfe?tab=readme-ov-file#mfe-development>`__.
The authentication micro-frontend also requires the following additional variable: The authentication micro-frontend also requires the following additional variable:
@@ -142,13 +148,13 @@ Furthermore, there are several edX-specific environment variables that enable in
- ``true`` | ``''`` (empty strings are falsy) - ``true`` | ``''`` (empty strings are falsy)
For more information see the document: `Micro-frontend applications in Open For more information see the document: `Micro-frontend applications in Open
edX <https://edx.readthedocs.io/projects/edx-developer-docs/en/latest/developers_guide/micro_frontends_in_open_edx.html#required-environment-variables>`__. edX <https://github.com/overhangio/tutor-mfe?tab=readme-ov-file#mfe-development>`__.
How To Contribute How To Contribute
================= =================
Contributions are very welcome, and strongly encouraged! We've Contributions are very welcome, and strongly encouraged! We've
put together `some documentation that describes our contribution process <https://edx.readthedocs.org/projects/edx-developer-guide/en/latest/process/index.html>`_. put together `some documentation that describes our contribution process <https://docs.openedx.org/en/latest/developers/references/developer_guide/process/index.html>`_.
Even though they were written with edx-platform in mind, the guidelines should be followed for Open edX code in general. Even though they were written with edx-platform in mind, the guidelines should be followed for Open edX code in general.
@@ -187,7 +193,7 @@ All community members are expected to follow the `Open edX Code of Conduct <http
People People
====== ======
The assigned maintainers for this component and other project details may be The assigned maintainers for this component and other project details may be
found in `Backstage <https://backstage.openedx.org/catalog/default/group/2u-vanguards>`_. Backstage pulls this data from the ``catalog-info.yaml`` found in `Backstage <https://backstage.openedx.org/catalog/default/group/2u-infinity>`_. Backstage pulls this data from the ``catalog-info.yaml``
file in this repo. file in this repo.
Reporting Security Issues Reporting Security Issues

View File

@@ -12,7 +12,8 @@ metadata:
icon: 'Article' icon: 'Article'
annotations: annotations:
openedx.org/arch-interest-groups: "" openedx.org/arch-interest-groups: ""
openedx.org/release: "master"
spec: spec:
owner: group:2u-vanguards owner: group:2u-infinity
type: 'service' type: 'service'
lifecycle: 'production' lifecycle: 'production'

View File

@@ -3,7 +3,7 @@ Enable Social Auth Locally
Please follow the steps below to enable social auth (SSO) locally. Please follow the steps below to enable social auth (SSO) locally.
1. Follow `Enabling Third Party Authentication <https://edx.readthedocs.io/projects/edx-installing-configuring-and-running/en/latest/configuration/tpa/index.html>`_ for backend configuration. 1. Follow `Enabling Third Party Authentication <https://docs.openedx.org/en/latest/site_ops/install_configure_run_guide/configuration/tpa/index.html>`_ for backend configuration.
2. Authn has a component for rendering Social Auth providers at frontend which goes through each provider. 2. Authn has a component for rendering Social Auth providers at frontend which goes through each provider.

60
example.env.config.js Normal file
View File

@@ -0,0 +1,60 @@
/*
Authn MFE is now able to handle JS-based configuration!
For the time being, the `.env.*` files are still made available when cloning down this repo or pulling from
the master branch. To switch to using `env.config.js`, make a copy of `example.env.config.js` and configure as needed.
For testing with Jest Snapshot, there is a mock in `/src/setupTest.jsx` for `getConfig` that will need to be
uncommented.
Note: having both .env and env.config.js files will follow a predictable order, in which non-empty values in the
JS-based config will overwrite the .env environment variables.
frontend-platform's getConfig loads configuration in the following sequence:
- .env file config
- optional handlers (commonly used to merge MFE-specific config in via additional process.env variables)
- env.config.js file config
- runtime config
*/
module.exports = {
NODE_ENV: 'development',
NODE_PATH: './src',
PORT: 1999,
ACCESS_TOKEN_COOKIE_NAME: 'edx-jwt-cookie-header-payload',
BASE_URL: 'http://localhost:1999',
CREDENTIALS_BASE_URL: 'http://localhost:18150',
CSRF_TOKEN_API_PATH: '/csrf/api/v1/token',
ECOMMERCE_BASE_URL: 'http://localhost:18130',
LANGUAGE_PREFERENCE_COOKIE_NAME: 'openedx-language-preference',
LMS_BASE_URL: 'http://localhost:18000',
LOGIN_URL: 'http://localhost:1999/login',
LOGOUT_URL: 'http://localhost:18000/logout',
LOGO_URL: 'https://edx-cdn.org/v3/default/logo.svg',
LOGO_TRADEMARK_URL: 'https://edx-cdn.org/v3/default/logo-trademark.svg',
LOGO_WHITE_URL: 'https://edx-cdn.org/v3/default/logo-white.svg',
FAVICON_URL: 'https://edx-cdn.org/v3/default/favicon.ico',
MARKETING_SITE_BASE_URL: 'http://localhost:18000',
ORDER_HISTORY_URL: 'http://localhost:1996/orders',
REFRESH_ACCESS_TOKEN_ENDPOINT: 'http://localhost:18000/login_refresh',
SEGMENT_KEY: '',
SITE_NAME: 'Your Platform Name Here',
INFO_EMAIL: 'info@example.com',
ENABLE_DYNAMIC_REGISTRATION_FIELDS: 'true',
ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN: 'true',
SESSION_COOKIE_DOMAIN: 'localhost',
USER_INFO_COOKIE_NAME: 'edx-user-info',
LOGIN_ISSUE_SUPPORT_LINK: 'http://localhost:18000/login-issue-support-url',
TOS_AND_HONOR_CODE: 'http://localhost:18000/honor',
TOS_LINK: 'http://localhost:18000/tos',
PRIVACY_POLICY: 'http://localhost:18000/privacy',
AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK: 'http://localhost:1999/welcome',
BANNER_IMAGE_LARGE: '',
BANNER_IMAGE_MEDIUM: '',
BANNER_IMAGE_SMALL: '',
BANNER_IMAGE_EXTRA_SMALL: '',
APP_ID: '',
MFE_CONFIG_API_URL: '',
ZENDESK_KEY: '',
ZENDESK_LOGO_URL: '',
};

View File

@@ -1,8 +0,0 @@
# This file describes this Open edX repo, as described in OEP-2:
# http://open-edx-proposals.readthedocs.io/en/latest/oeps/oep-0002.html#specification
nick: Authn MFE
oeps: {}
owner: openedx/2u-vanguards
openedx-release:
ref: master

30216
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -13,15 +13,12 @@
"build": "fedx-scripts webpack", "build": "fedx-scripts webpack",
"i18n_extract": "fedx-scripts formatjs extract", "i18n_extract": "fedx-scripts formatjs extract",
"lint": "fedx-scripts eslint --ext .js --ext .jsx .", "lint": "fedx-scripts eslint --ext .js --ext .jsx .",
"lint:fix": "fedx-scripts eslint --fix --ext .js --ext .jsx .",
"snapshot": "fedx-scripts jest --updateSnapshot", "snapshot": "fedx-scripts jest --updateSnapshot",
"start": "fedx-scripts webpack-dev-server --progress", "start": "fedx-scripts webpack-dev-server --progress",
"dev": "PUBLIC_PATH=/authn/ MFE_CONFIG_API_URL='http://localhost:8000/api/mfe_config/v1' fedx-scripts webpack-dev-server --progress --host apps.local.openedx.io",
"test": "fedx-scripts jest --coverage --passWithNoTests" "test": "fedx-scripts jest --coverage --passWithNoTests"
}, },
"husky": {
"hooks": {
"pre-commit": "npm run lint"
}
},
"author": "edX", "author": "edX",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"homepage": "https://github.com/openedx/frontend-app-authn#readme", "homepage": "https://github.com/openedx/frontend-app-authn#readme",
@@ -33,53 +30,56 @@
}, },
"dependencies": { "dependencies": {
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2", "@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
"@edx/frontend-platform": "7.1.3", "@edx/frontend-platform": "^8.3.1",
"@edx/openedx-atlas": "^0.6.0", "@edx/openedx-atlas": "^0.6.0",
"@fortawesome/fontawesome-svg-core": "6.5.2", "@fortawesome/fontawesome-svg-core": "6.7.2",
"@fortawesome/free-brands-svg-icons": "6.5.2", "@fortawesome/free-brands-svg-icons": "6.7.2",
"@fortawesome/free-solid-svg-icons": "6.5.2", "@fortawesome/free-solid-svg-icons": "6.7.2",
"@fortawesome/react-fontawesome": "0.2.0", "@openedx/frontend-plugin-framework": "^1.3.0",
"@openedx/paragon": "^22.1.1", "@fortawesome/react-fontawesome": "0.2.6",
"@openedx/paragon": "^23.4.2",
"@optimizely/react-sdk": "^2.9.1", "@optimizely/react-sdk": "^2.9.1",
"@redux-devtools/extension": "3.3.0", "@redux-devtools/extension": "3.3.0",
"@testing-library/react": "^12.1.5", "@testing-library/react": "^16.2.0",
"@testing-library/react-hooks": "^8.0.1",
"algoliasearch": "^4.14.3", "algoliasearch": "^4.14.3",
"algoliasearch-helper": "^3.14.0", "algoliasearch-helper": "^3.26.0",
"classnames": "2.5.1", "classnames": "2.5.1",
"core-js": "3.36.1", "core-js": "3.43.0",
"fastest-levenshtein": "1.0.16", "fastest-levenshtein": "1.0.16",
"form-urlencoded": "6.1.4", "form-urlencoded": "6.1.6",
"prop-types": "15.8.1", "prop-types": "15.8.1",
"query-string": "7.1.3", "query-string": "7.1.3",
"react": "^17.0.2", "react-error-boundary": "^4.0.13",
"react-dom": "^17.0.2", "react": "^18.3.1",
"react-dom": "^18.3.1",
"react-google-recaptcha-v3": "^1.11.0",
"react-helmet": "6.1.0", "react-helmet": "6.1.0",
"react-loading-skeleton": "3.4.0", "react-loading-skeleton": "3.5.0",
"react-redux": "7.2.9", "react-redux": "7.2.9",
"react-responsive": "8.2.0", "react-responsive": "8.2.0",
"react-router": "6.22.3", "react-router": "6.30.1",
"react-router-dom": "6.22.3", "react-router-dom": "6.30.1",
"react-zendesk": "^0.1.13", "react-zendesk": "^0.1.13",
"redux": "4.2.0", "redux": "4.2.1",
"redux-logger": "3.0.6", "redux-logger": "3.0.6",
"redux-mock-store": "1.5.4", "redux-mock-store": "1.5.5",
"redux-saga": "1.3.0", "redux-saga": "1.3.0",
"redux-thunk": "2.4.2", "redux-thunk": "2.4.2",
"regenerator-runtime": "0.14.1", "regenerator-runtime": "0.14.1",
"reselect": "4.1.8", "reselect": "5.1.1",
"universal-cookie": "4.0.4" "universal-cookie": "7.2.2"
}, },
"devDependencies": { "devDependencies": {
"@edx/browserslist-config": "^1.1.1", "@edx/browserslist-config": "^1.1.1",
"@edx/reactifex": "1.1.0", "@edx/reactifex": "1.1.0",
"@openedx/frontend-build": "13.1.4", "copy-webpack-plugin": "^11.0.0",
"babel-plugin-formatjs": "10.5.14", "@openedx/frontend-build": "^14.6.2",
"eslint-plugin-import": "2.29.1", "babel-plugin-formatjs": "10.5.39",
"eslint-plugin-import": "2.32.0",
"glob": "7.2.3", "glob": "7.2.3",
"history": "5.3.0", "history": "5.3.0",
"husky": "7.0.4", "jest": "30.1.3",
"jest": "29.7.0", "react-test-renderer": "^18.3.1",
"react-test-renderer": "^17.0.2" "ts-jest": "^29.4.0"
} }
} }

View File

@@ -1,25 +1,32 @@
<!doctype html> <!DOCTYPE html>
<html lang="en-us"> <html lang="en-us">
<head> <head>
<title><%= (process.env.SITE_NAME && process.env.SITE_NAME != 'null') ? 'Authentication | ' + process.env.SITE_NAME : 'Authentication' %></title> <title>
<meta charset="utf-8"> <%= (process.env.SITE_NAME && process.env.SITE_NAME !='null' ) ?
<meta name="viewport" content="width=device-width, initial-scale=1.0"> 'Authentication | ' + process.env.SITE_NAME : 'Authentication' %>
<link rel="shortcut icon" href="<%=htmlWebpackPlugin.options.FAVICON_URL%>" type="image/x-icon"/> </title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/iframe-resizer/4.3.9/iframeResizer.contentWindow.min.js" <meta charset="utf-8" />
integrity="sha512-mdT/HQRzoRP4laVz49Mndx6rcCGA3IhuyhP3gaY0E9sZPkwbtDk9ttQIq9o8qGCf5VvJv1Xsy3k2yTjfUoczqw==" <meta name="viewport" content="width=device-width, initial-scale=1.0" />
crossorigin="anonymous" <meta name="robots" content="noindex, nofollow" />
referrerpolicy="no-referrer"> <link
</script> rel="shortcut icon"
<% if (process.env.OPTIMIZELY_URL) { %> href="<%=htmlWebpackPlugin.options.FAVICON_URL%>"
<script type="image/x-icon"
src="<%= process.env.OPTIMIZELY_URL %>" />
></script> <script defer src="https://www.edx.org/beam-wrapper.js" ></script>
<% } else if (process.env.OPTIMIZELY_PROJECT_ID) { %> <script
<script src="https://cdnjs.cloudflare.com/ajax/libs/iframe-resizer/4.4.4/iframeResizer.contentWindow.min.js"
src="<%= process.env.MARKETING_SITE_BASE_URL %>/optimizelyjs/<%= process.env.OPTIMIZELY_PROJECT_ID %>.js" integrity="sha512-IWwZFBvHzN41wNI6etRLLuLrDDj/6AwJcPt7cmKJAzluYTIHHQ1PF8wh0rSy05jxEvvjflVvH2MxeV6riyEEXg=="
></script> crossorigin="anonymous"
<% } %> referrerpolicy="no-referrer"
></script>
<% if (process.env.OPTIMIZELY_URL) { %>
<script src="<%= process.env.OPTIMIZELY_URL %>"></script>
<% } else if (process.env.OPTIMIZELY_PROJECT_ID) { %>
<script src="<%= process.env.MARKETING_SITE_BASE_URL %>/optimizelyjs/<%= process.env.OPTIMIZELY_PROJECT_ID %>.js"></script>
<% } %>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
</body> </body>

2
public/robots.txt Normal file
View File

@@ -0,0 +1,2 @@
User-agent: *
Disallow: /

View File

@@ -2,11 +2,12 @@ import React from 'react';
import { getConfig } from '@edx/frontend-platform'; import { getConfig } from '@edx/frontend-platform';
import { AppProvider } from '@edx/frontend-platform/react'; import { AppProvider } from '@edx/frontend-platform/react';
import { GoogleReCaptchaProvider } from 'react-google-recaptcha-v3';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import { Navigate, Route, Routes } from 'react-router-dom'; import { Navigate, Route, Routes } from 'react-router-dom';
import { import {
EmbeddedRegistrationRoute, NotFoundPage, registerIcons, UnAuthOnlyRoute, Zendesk, EmbeddedRegistrationRoute, NotFoundPage, registerIcons, RouteTracker, UnAuthOnlyRoute,
} from './common-components'; } from './common-components';
import configureStore from './data/configureStore'; import configureStore from './data/configureStore';
import { import {
@@ -22,6 +23,7 @@ import {
import { updatePathWithQueryParams } from './data/utils'; import { updatePathWithQueryParams } from './data/utils';
import { ForgotPasswordPage } from './forgot-password'; import { ForgotPasswordPage } from './forgot-password';
import Logistration from './logistration/Logistration'; import Logistration from './logistration/Logistration';
import MainAppSlot from './plugin-slots/MainAppSlot';
import { ProgressiveProfiling } from './progressive-profiling'; import { ProgressiveProfiling } from './progressive-profiling';
import { RecommendationsPage } from './recommendations'; import { RecommendationsPage } from './recommendations';
import { RegistrationPage } from './register'; import { RegistrationPage } from './register';
@@ -31,33 +33,43 @@ import './index.scss';
registerIcons(); registerIcons();
const MainApp = () => ( const MainApp = () => {
<AppProvider store={configureStore()}> const recaptchaKey = getConfig().RECAPTCHA_SITE_KEY_WEB;
<Helmet>
<link rel="shortcut icon" href={getConfig().FAVICON_URL} type="image/x-icon" /> return (
</Helmet> <GoogleReCaptchaProvider
{getConfig().ZENDESK_KEY && <Zendesk />} reCaptchaKey={recaptchaKey}
<Routes> useEnterprise
<Route path="/" element={<Navigate replace to={updatePathWithQueryParams(REGISTER_PAGE)} />} /> >
<Route <AppProvider store={configureStore()}>
path={REGISTER_EMBEDDED_PAGE} <Helmet>
element={<EmbeddedRegistrationRoute><RegistrationPage /></EmbeddedRegistrationRoute>} <link rel="shortcut icon" href={getConfig().FAVICON_URL} type="image/x-icon" />
/> </Helmet>
<Route <Routes>
path={LOGIN_PAGE} <Route path="/" element={<Navigate replace to={updatePathWithQueryParams(REGISTER_PAGE)} />} />
element={ <Route
<UnAuthOnlyRoute><Logistration selectedPage={LOGIN_PAGE} /></UnAuthOnlyRoute> path={REGISTER_EMBEDDED_PAGE}
} element={<EmbeddedRegistrationRoute><RegistrationPage /></EmbeddedRegistrationRoute>}
/> />
<Route path={REGISTER_PAGE} element={<UnAuthOnlyRoute><Logistration /></UnAuthOnlyRoute>} /> <Route
<Route path={RESET_PAGE} element={<UnAuthOnlyRoute><ForgotPasswordPage /></UnAuthOnlyRoute>} /> path={LOGIN_PAGE}
<Route path={PASSWORD_RESET_CONFIRM} element={<ResetPasswordPage />} /> element={
<Route path={AUTHN_PROGRESSIVE_PROFILING} element={<ProgressiveProfiling />} /> <UnAuthOnlyRoute><Logistration selectedPage={LOGIN_PAGE} /></UnAuthOnlyRoute>
<Route path={RECOMMENDATIONS} element={<RecommendationsPage />} /> }
<Route path={PAGE_NOT_FOUND} element={<NotFoundPage />} /> />
<Route path="*" element={<Navigate replace to={PAGE_NOT_FOUND} />} /> <Route path={REGISTER_PAGE} element={<UnAuthOnlyRoute><Logistration /></UnAuthOnlyRoute>} />
</Routes> <Route path={RESET_PAGE} element={<UnAuthOnlyRoute><ForgotPasswordPage /></UnAuthOnlyRoute>} />
</AppProvider> <Route path={PASSWORD_RESET_CONFIRM} element={<ResetPasswordPage />} />
); <Route path={AUTHN_PROGRESSIVE_PROFILING} element={<ProgressiveProfiling />} />
<Route path={RECOMMENDATIONS} element={<RecommendationsPage />} />
<Route path={PAGE_NOT_FOUND} element={<NotFoundPage />} />
<Route path="*" element={<Navigate replace to={PAGE_NOT_FOUND} />} />
</Routes>
<RouteTracker />
<MainAppSlot />
</AppProvider>
</GoogleReCaptchaProvider>
);
};
export default MainApp; export default MainApp;

22
src/cohesion/constants.js Normal file
View File

@@ -0,0 +1,22 @@
export const PAGE_TYPES = {
ACCOUNT_CREATION: 'account-creation',
SIGN_IN: 'sign-in',
};
export const ELEMENT_TYPES = {
BUTTON: 'BUTTON',
};
export const EVENT_TYPES = { ElementClicked: 'redventures.usertracking.v3.ElementClicked' };
export const ELEMENT_TEXT = {
CREATE_ACCOUNT: 'create-account',
OPT_IN_TEXT: 'I agree that edx may send me marketing messages',
SIGN_IN: 'Sign In',
};
export const ELEMENT_NAME = {
SIGN_IN: PAGE_TYPES.SIGN_IN,
OPT_OUT: 'opt-out',
CREATE_ACCOUNT: 'Create an account for free',
};

View File

@@ -0,0 +1,6 @@
export const SET_COHESION_EVENT_ELEMENT_STATES = 'SET_COHESION_EVENT_ELEMENT_STATES';
export const setCohesionEventStates = (eventData) => ({
type: SET_COHESION_EVENT_ELEMENT_STATES,
payload: eventData,
});

View File

@@ -0,0 +1,17 @@
import { SET_COHESION_EVENT_ELEMENT_STATES } from './actions';
export const storeName = 'cohesion';
export const defaultState = {
eventData: {},
};
export const reducer = (state = defaultState, action = {}) => {
if (action.type === SET_COHESION_EVENT_ELEMENT_STATES) {
return {
...state,
eventData: action.payload,
};
}
return state;
};

24
src/cohesion/trackers.js Normal file
View File

@@ -0,0 +1,24 @@
import { EVENT_TYPES } from './constants';
/**
* Tracks cohesion events by setting the page type and tracking a click event.
*
* @param {string} pageType - The type of page where the event occurred.
* @param {string} elementType - The type of the web element (e.g., 'BUTTON', 'LINK').
* @param {string} webElementText - The text content of the web element.
* @param {string} webElementName - The name of the web element.
*/
const trackCohesionEvent = (eventData) => {
window.chsn_pageType = eventData.pageType;
const webElement = {
elementType: eventData.elementType,
text: eventData.webElementText,
name: eventData.webElementName,
};
window.tagular?.('beam', {
'@type': EVENT_TYPES.ElementClicked,
webElement,
});
};
export default trackCohesionEvent;

6
src/cohesion/utils.js Normal file
View File

@@ -0,0 +1,6 @@
const mockTagular = () => {
const getTagular = jest.fn();
window.tagular = getTagular;
};
export default mockTagular;

View File

@@ -60,7 +60,7 @@ const InstitutionLogistration = props => {
className="btn nav-item p-0 mb-1 institutions--provider-link" className="btn nav-item p-0 mb-1 institutions--provider-link"
destination={lmsBaseUrl + provider.loginUrl} destination={lmsBaseUrl + provider.loginUrl}
> >
{provider.name} {provider?.name}
</Hyperlink> </Hyperlink>
</td> </td>
</tr> </tr>

View File

@@ -1,11 +1,15 @@
import { useSelector } from 'react-redux';
import { getConfig } from '@edx/frontend-platform'; import { getConfig } from '@edx/frontend-platform';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Navigate } from 'react-router-dom'; import { Navigate } from 'react-router-dom';
import trackCohesionEvent from '../cohesion/trackers';
import { import {
AUTHN_PROGRESSIVE_PROFILING, RECOMMENDATIONS, REDIRECT, AUTHN_PROGRESSIVE_PROFILING, RECOMMENDATIONS, REDIRECT,
} from '../data/constants'; } from '../data/constants';
import { setCookie } from '../data/utils'; import setCookie from '../data/utils/cookies';
import { redirectWithDelay } from '../data/utils/dataUtils';
const RedirectLogistration = (props) => { const RedirectLogistration = (props) => {
const { const {
@@ -20,10 +24,16 @@ const RedirectLogistration = (props) => {
userId, userId,
registrationEmbedded, registrationEmbedded,
host, host,
currectProvider,
} = props; } = props;
const cohesionEventData = useSelector(state => state.cohesion.eventData);
let finalRedirectUrl = ''; let finalRedirectUrl = '';
if (success) { if (success) {
// This event is used by cohesion upon successful login and registration
if (!currectProvider) {
trackCohesionEvent(cohesionEventData);
}
// If we're in a third party auth pipeline, we must complete the pipeline // If we're in a third party auth pipeline, we must complete the pipeline
// once user has successfully logged in. Otherwise, redirect to the specified redirect url. // once user has successfully logged in. Otherwise, redirect to the specified redirect url.
// Note: For multiple enterprise use case, we need to make sure that user first visits the // Note: For multiple enterprise use case, we need to make sure that user first visits the
@@ -75,8 +85,7 @@ const RedirectLogistration = (props) => {
/> />
); );
} }
redirectWithDelay(finalRedirectUrl);
window.location.href = finalRedirectUrl;
} }
return null; return null;
@@ -94,6 +103,7 @@ RedirectLogistration.defaultProps = {
userId: null, userId: null,
registrationEmbedded: false, registrationEmbedded: false,
host: '', host: '',
currectProvider: '',
}; };
RedirectLogistration.propTypes = { RedirectLogistration.propTypes = {
@@ -108,6 +118,7 @@ RedirectLogistration.propTypes = {
userId: PropTypes.number, userId: PropTypes.number,
registrationEmbedded: PropTypes.bool, registrationEmbedded: PropTypes.bool,
host: PropTypes.string, host: PropTypes.string,
currectProvider: PropTypes.string,
}; };
export default RedirectLogistration; export default RedirectLogistration;

View File

@@ -0,0 +1,15 @@
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
const RouteTracker = () => {
const location = useLocation();
useEffect(() => {
window.tagular?.('pageView');
}, [location]);
return null;
};
export default RouteTracker;

View File

@@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import { useSelector } from 'react-redux';
import { getConfig } from '@edx/frontend-platform'; import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n'; import { useIntl } from '@edx/frontend-platform/i18n';
@@ -8,17 +9,35 @@ import { Login } from '@openedx/paragon/icons';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import messages from './messages'; import messages from './messages';
import { LOGIN_PAGE, SUPPORTED_ICON_CLASSES } from '../data/constants'; import { ELEMENT_TYPES, PAGE_TYPES } from '../cohesion/constants';
import trackCohesionEvent from '../cohesion/trackers';
import {
LOGIN_PAGE, REGISTER_PAGE, SUPPORTED_ICON_CLASSES,
} from '../data/constants';
import { setCookie } from '../data/utils';
import { redirectWithDelay } from '../data/utils/dataUtils';
const SocialAuthProviders = (props) => { const SocialAuthProviders = (props) => {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const { referrer, socialAuthProviders } = props; const { referrer, socialAuthProviders } = props;
const registrationFields = useSelector(state => state.register.registrationFormData);
function handleSubmit(e) { function handleSubmit(e, providerName) {
e.preventDefault(); e.preventDefault();
const eventData = {
pageType: referrer === LOGIN_PAGE ? PAGE_TYPES.SIGN_IN : PAGE_TYPES.ACCOUNT_CREATION,
elementType: ELEMENT_TYPES.BUTTON,
webElementText: providerName,
webElementName: providerName.toLowerCase(),
};
// This event is used by cohesion upon successful login
trackCohesionEvent(eventData);
if (referrer === REGISTER_PAGE) {
setCookie('marketingEmailsOptIn', registrationFields?.configurableFormFields?.marketingEmailsOptIn);
}
const url = e.currentTarget.dataset.providerUrl; const url = e.currentTarget.dataset.providerUrl;
window.location.href = getConfig().LMS_BASE_URL + url; redirectWithDelay(getConfig().LMS_BASE_URL + url);
} }
const socialAuth = socialAuthProviders.map((provider, index) => ( const socialAuth = socialAuthProviders.map((provider, index) => (
@@ -28,7 +47,7 @@ const SocialAuthProviders = (props) => {
type="button" type="button"
className={`btn-social btn-${provider.id} ${index % 2 === 0 ? 'mr-3' : ''}`} className={`btn-social btn-${provider.id} ${index % 2 === 0 ? 'mr-3' : ''}`}
data-provider-url={referrer === LOGIN_PAGE ? provider.loginUrl : provider.registerUrl} data-provider-url={referrer === LOGIN_PAGE ? provider.loginUrl : provider.registerUrl}
onClick={handleSubmit} onClick={(event) => handleSubmit(event, provider?.name)}
> >
{provider.iconImage ? ( {provider.iconImage ? (
<div aria-hidden="true"> <div aria-hidden="true">

View File

@@ -37,6 +37,7 @@ const ThirdPartyAuth = (props) => {
const isSocialAuthActive = !!providers.length && !currentProvider; const isSocialAuthActive = !!providers.length && !currentProvider;
const isEnterpriseLoginDisabled = getConfig().DISABLE_ENTERPRISE_LOGIN; const isEnterpriseLoginDisabled = getConfig().DISABLE_ENTERPRISE_LOGIN;
const enterpriseLoginURL = getConfig().LMS_BASE_URL + ENTERPRISE_LOGIN_URL; const enterpriseLoginURL = getConfig().LMS_BASE_URL + ENTERPRISE_LOGIN_URL;
const isThirdPartyAuthActive = isSocialAuthActive || (isEnterpriseLoginDisabled && isInstitutionAuthActive);
return ( return (
<> <>
@@ -61,7 +62,7 @@ const ThirdPartyAuth = (props) => {
</Hyperlink> </Hyperlink>
)} )}
{thirdPartyAuthApiStatus === PENDING_STATE ? ( {thirdPartyAuthApiStatus === PENDING_STATE && isThirdPartyAuthActive ? (
<div className="mt-4"> <div className="mt-4">
<Skeleton className="tpa-skeleton" height={36} count={2} /> <Skeleton className="tpa-skeleton" height={36} count={2} />
</div> </div>

View File

@@ -7,6 +7,7 @@ import PropTypes from 'prop-types';
import messages from './messages'; import messages from './messages';
import { LOGIN_PAGE, REGISTER_PAGE } from '../data/constants'; import { LOGIN_PAGE, REGISTER_PAGE } from '../data/constants';
import setCookie from '../data/utils/cookies';
const ThirdPartyAuthAlert = (props) => { const ThirdPartyAuthAlert = (props) => {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
@@ -20,7 +21,10 @@ const ThirdPartyAuthAlert = (props) => {
message = formatMessage(messages['register.third.party.auth.account.not.linked'], { currentProvider, platformName }); message = formatMessage(messages['register.third.party.auth.account.not.linked'], { currentProvider, platformName });
} }
if (!currentProvider) { if (currentProvider) {
// Setting this cookie to capture marketingEmailsOptIn for SSO flow on the onboarding component
setCookie('ssoPipelineRedirectionDone', true);
} else {
return null; return null;
} }

View File

@@ -4,9 +4,8 @@ import { getConfig } from '@edx/frontend-platform';
import { fetchAuthenticatedUser, getAuthenticatedUser } from '@edx/frontend-platform/auth'; import { fetchAuthenticatedUser, getAuthenticatedUser } from '@edx/frontend-platform/auth';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { import { RESET_PAGE } from '../data/constants';
DEFAULT_REDIRECT_URL, import { updatePathWithQueryParams } from '../data/utils';
} from '../data/constants';
/** /**
* This wrapper redirects the requester to our default redirect url if they are * This wrapper redirects the requester to our default redirect url if they are
@@ -25,7 +24,12 @@ const UnAuthOnlyRoute = ({ children }) => {
if (isReady) { if (isReady) {
if (authUser && authUser.username) { if (authUser && authUser.username) {
global.location.href = getConfig().LMS_BASE_URL.concat(DEFAULT_REDIRECT_URL); const updatedPath = updatePathWithQueryParams(window.location.pathname);
if (updatedPath.startsWith(RESET_PAGE)) {
global.location.href = getConfig().LMS_BASE_URL;
return null;
}
global.location.href = getConfig().LMS_BASE_URL.concat(updatedPath);
return null; return null;
} }

View File

@@ -13,9 +13,15 @@ export const getThirdPartyAuthContextBegin = () => ({
type: THIRD_PARTY_AUTH_CONTEXT.BEGIN, type: THIRD_PARTY_AUTH_CONTEXT.BEGIN,
}); });
export const getThirdPartyAuthContextSuccess = (fieldDescriptions, optionalFields, thirdPartyAuthContext) => ({ export const getThirdPartyAuthContextSuccess = (
fieldDescriptions,
optionalFields,
thirdPartyAuthContext,
countriesCodesList) => ({
type: THIRD_PARTY_AUTH_CONTEXT.SUCCESS, type: THIRD_PARTY_AUTH_CONTEXT.SUCCESS,
payload: { fieldDescriptions, optionalFields, thirdPartyAuthContext }, payload: {
fieldDescriptions, optionalFields, thirdPartyAuthContext, countriesCodesList,
},
}); });
export const getThirdPartyAuthContextFailure = () => ({ export const getThirdPartyAuthContextFailure = () => ({

View File

@@ -0,0 +1,83 @@
export const registerFields = {
fields: {
country: {
name: 'country',
error_message: 'Select your country or region of residence',
},
honor_code: {
name: 'honor_code',
type: 'tos_and_honor_code',
error_message: '',
},
},
};
export const progressiveProfilingFields = {
extended_profile: [],
fields: {
level_of_education: {
name: 'level_of_education',
type: 'select',
label: 'Highest level of education completed',
error_message: '',
options: [
[
'p',
'Doctorate',
],
[
'm',
"Master's or professional degree",
],
[
'b',
"Bachelor's degree",
],
[
'a',
'Associate degree',
],
[
'hs',
'Secondary/high school',
],
[
'jhs',
'Junior secondary/junior high/middle school',
],
[
'none',
'No formal education',
],
[
'other',
'Other education',
],
],
},
gender: {
name: 'gender',
type: 'select',
label: 'Gender',
error_message: '',
options: [
[
'm',
'Male',
],
[
'f',
'Female',
],
[
'o',
'Other/Prefer Not to Say',
],
],
},
},
};
export const FIELD_LABELS = {
COUNTRY: 'country',
};

View File

@@ -35,6 +35,7 @@ const reducer = (state = defaultState, action = {}) => {
optionalFields: action.payload.optionalFields, optionalFields: action.payload.optionalFields,
thirdPartyAuthContext: action.payload.thirdPartyAuthContext, thirdPartyAuthContext: action.payload.thirdPartyAuthContext,
thirdPartyAuthApiStatus: COMPLETE_STATE, thirdPartyAuthApiStatus: COMPLETE_STATE,
countriesCodesList: action.payload.countriesCodesList,
}; };
} }
case THIRD_PARTY_AUTH_CONTEXT.FAILURE: case THIRD_PARTY_AUTH_CONTEXT.FAILURE:

View File

@@ -1,3 +1,4 @@
import { getConfig } from '@edx/frontend-platform';
import { logError } from '@edx/frontend-platform/logging'; import { logError } from '@edx/frontend-platform/logging';
import { call, put, takeEvery } from 'redux-saga/effects'; import { call, put, takeEvery } from 'redux-saga/effects';
@@ -7,7 +8,9 @@ import {
getThirdPartyAuthContextSuccess, getThirdPartyAuthContextSuccess,
THIRD_PARTY_AUTH_CONTEXT, THIRD_PARTY_AUTH_CONTEXT,
} from './actions'; } from './actions';
import { progressiveProfilingFields, registerFields } from './constants';
import { import {
getCountryList,
getThirdPartyAuthContext, getThirdPartyAuthContext,
} from './service'; } from './service';
import { setCountryFromThirdPartyAuthContext } from '../../register/data/actions'; import { setCountryFromThirdPartyAuthContext } from '../../register/data/actions';
@@ -18,9 +21,25 @@ export function* fetchThirdPartyAuthContext(action) {
const { const {
fieldDescriptions, optionalFields, thirdPartyAuthContext, fieldDescriptions, optionalFields, thirdPartyAuthContext,
} = yield call(getThirdPartyAuthContext, action.payload.urlParams); } = yield call(getThirdPartyAuthContext, action.payload.urlParams);
const countriesCodesList = (yield call(getCountryList)) || [];
yield put(setCountryFromThirdPartyAuthContext(thirdPartyAuthContext.countryCode)); yield put(setCountryFromThirdPartyAuthContext(thirdPartyAuthContext.countryCode));
yield put(getThirdPartyAuthContextSuccess(fieldDescriptions, optionalFields, thirdPartyAuthContext)); // hard code country field, level of education and gender fields
if (getConfig().ENABLE_HARD_CODE_OPTIONAL_FIELDS) {
yield put(getThirdPartyAuthContextSuccess(
registerFields,
progressiveProfilingFields,
thirdPartyAuthContext,
countriesCodesList,
));
} else {
yield put(getThirdPartyAuthContextSuccess(
fieldDescriptions,
optionalFields,
thirdPartyAuthContext,
countriesCodesList,
));
}
} catch (e) { } catch (e) {
yield put(getThirdPartyAuthContextFailure()); yield put(getThirdPartyAuthContextFailure());
logError(e); logError(e);

View File

@@ -1,5 +1,8 @@
import { getConfig } from '@edx/frontend-platform'; import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { logError } from '@edx/frontend-platform/logging';
import { FIELD_LABELS } from './constants';
// eslint-disable-next-line import/prefer-default-export // eslint-disable-next-line import/prefer-default-export
export async function getThirdPartyAuthContext(urlParams) { export async function getThirdPartyAuthContext(urlParams) {
@@ -23,3 +26,28 @@ export async function getThirdPartyAuthContext(urlParams) {
thirdPartyAuthContext: data.contextData || {}, thirdPartyAuthContext: data.contextData || {},
}; };
} }
function extractCountryList(data) {
return data?.fields
.find(({ name }) => name === FIELD_LABELS.COUNTRY)
?.options?.map(({ value }) => (value)) || [];
}
export async function getCountryList() {
try {
const requestConfig = {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
isPublic: true,
};
const { data } = await getAuthenticatedHttpClient()
.get(
`${getConfig().LMS_BASE_URL}/user_api/v1/account/registration/`,
requestConfig,
);
return extractCountryList(data);
} catch (e) {
logError(e);
return [];
}
}

View File

@@ -8,6 +8,11 @@ import * as api from '../service';
const { loggingService } = initializeMockLogging(); const { loggingService } = initializeMockLogging();
jest.mock('../service', () => ({
getCountryList: jest.fn(),
getThirdPartyAuthContext: jest.fn(),
}));
describe('fetchThirdPartyAuthContext', () => { describe('fetchThirdPartyAuthContext', () => {
const params = { const params = {
payload: { urlParams: {} }, payload: { urlParams: {} },
@@ -31,6 +36,7 @@ describe('fetchThirdPartyAuthContext', () => {
thirdPartyAuthContext: data, thirdPartyAuthContext: data,
fieldDescriptions: {}, fieldDescriptions: {},
optionalFields: {}, optionalFields: {},
countriesCodesList: [],
})); }));
const dispatched = []; const dispatched = [];
@@ -44,7 +50,7 @@ describe('fetchThirdPartyAuthContext', () => {
expect(dispatched).toEqual([ expect(dispatched).toEqual([
actions.getThirdPartyAuthContextBegin(), actions.getThirdPartyAuthContextBegin(),
setCountryFromThirdPartyAuthContext(), setCountryFromThirdPartyAuthContext(),
actions.getThirdPartyAuthContextSuccess({}, {}, data), actions.getThirdPartyAuthContextSuccess({}, {}, data, []),
]); ]);
getThirdPartyAuthContext.mockClear(); getThirdPartyAuthContext.mockClear();
}); });

View File

@@ -2,6 +2,7 @@ export { default as RedirectLogistration } from './RedirectLogistration';
export { default as registerIcons } from './RegisterFaIcons'; export { default as registerIcons } from './RegisterFaIcons';
export { default as EmbeddedRegistrationRoute } from './EmbeddedRegistrationRoute'; export { default as EmbeddedRegistrationRoute } from './EmbeddedRegistrationRoute';
export { default as UnAuthOnlyRoute } from './UnAuthOnlyRoute'; export { default as UnAuthOnlyRoute } from './UnAuthOnlyRoute';
export { default as RouteTracker } from './RouteTracker';
export { default as NotFoundPage } from './NotFoundPage'; export { default as NotFoundPage } from './NotFoundPage';
export { default as SocialAuthProviders } from './SocialAuthProviders'; export { default as SocialAuthProviders } from './SocialAuthProviders';
export { default as ThirdPartyAuthAlert } from './ThirdPartyAuthAlert'; export { default as ThirdPartyAuthAlert } from './ThirdPartyAuthAlert';

View File

@@ -5,14 +5,13 @@ import React from 'react';
import { getConfig } from '@edx/frontend-platform'; import { getConfig } from '@edx/frontend-platform';
import { render } from '@testing-library/react'; import { render } from '@testing-library/react';
import { act } from 'react-dom/test-utils'; import { act } from 'react-dom/test-utils';
import { PAGE_NOT_FOUND, REGISTER_EMBEDDED_PAGE } from '../../data/constants';
import EmbeddedRegistrationRoute from '../EmbeddedRegistrationRoute';
import { import {
MemoryRouter, Route, BrowserRouter as Router, Routes, MemoryRouter, Route, BrowserRouter as Router, Routes,
} from 'react-router-dom'; } from 'react-router-dom';
import { PAGE_NOT_FOUND, REGISTER_EMBEDDED_PAGE } from '../../data/constants';
import EmbeddedRegistrationRoute from '../EmbeddedRegistrationRoute';
const RRD = require('react-router-dom'); const RRD = require('react-router-dom');
// Just render plain div with its children // Just render plain div with its children
// eslint-disable-next-line react/prop-types // eslint-disable-next-line react/prop-types

View File

@@ -1,7 +1,6 @@
import React from 'react';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n'; import { IntlProvider } from '@edx/frontend-platform/i18n';
import { fireEvent, render } from '@testing-library/react'; import { fireEvent, render } from '@testing-library/react';
import { act } from 'react-dom/test-utils'; import { act } from 'react-dom/test-utils';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
@@ -37,7 +36,6 @@ describe('FormGroup', () => {
describe('PasswordField', () => { describe('PasswordField', () => {
const mockStore = configureStore(); const mockStore = configureStore();
const IntlPasswordField = injectIntl(PasswordField);
let props = {}; let props = {};
let store = {}; let store = {};
@@ -66,7 +64,7 @@ describe('PasswordField', () => {
}); });
it('should show/hide password on icon click', () => { it('should show/hide password on icon click', () => {
const { getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />)); const { getByLabelText } = render(reduxWrapper(<PasswordField {...props} />));
const passwordInput = getByLabelText('Password'); const passwordInput = getByLabelText('Password');
const showPasswordButton = getByLabelText('Show password'); const showPasswordButton = getByLabelText('Show password');
@@ -79,7 +77,7 @@ describe('PasswordField', () => {
}); });
it('should show password requirement tooltip on focus', async () => { it('should show password requirement tooltip on focus', async () => {
const { getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />)); const { getByLabelText } = render(reduxWrapper(<PasswordField {...props} />));
const passwordInput = getByLabelText('Password'); const passwordInput = getByLabelText('Password');
jest.useFakeTimers(); jest.useFakeTimers();
await act(async () => { await act(async () => {
@@ -96,7 +94,7 @@ describe('PasswordField', () => {
...props, ...props,
value: '', value: '',
}; };
const { getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />)); const { getByLabelText } = render(reduxWrapper(<PasswordField {...props} />));
const passwordInput = getByLabelText('Password'); const passwordInput = getByLabelText('Password');
jest.useFakeTimers(); jest.useFakeTimers();
await act(async () => { await act(async () => {
@@ -119,7 +117,7 @@ describe('PasswordField', () => {
}); });
it('should update password requirement checks', async () => { it('should update password requirement checks', async () => {
const { getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />)); const { getByLabelText } = render(reduxWrapper(<PasswordField {...props} />));
const passwordInput = getByLabelText('Password'); const passwordInput = getByLabelText('Password');
jest.useFakeTimers(); jest.useFakeTimers();
await act(async () => { await act(async () => {
@@ -142,7 +140,7 @@ describe('PasswordField', () => {
}); });
it('should not run validations when blur is fired on password icon click', () => { it('should not run validations when blur is fired on password icon click', () => {
const { container, getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />)); const { container, getByLabelText } = render(reduxWrapper(<PasswordField {...props} />));
const passwordInput = container.querySelector('input[name="password"]'); const passwordInput = container.querySelector('input[name="password"]');
const passwordIcon = getByLabelText('Show password'); const passwordIcon = getByLabelText('Show password');
@@ -163,7 +161,7 @@ describe('PasswordField', () => {
...props, ...props,
handleBlur: jest.fn(), handleBlur: jest.fn(),
}; };
const { container } = render(reduxWrapper(<IntlPasswordField {...props} />)); const { container } = render(reduxWrapper(<PasswordField {...props} />));
const passwordInput = container.querySelector('input[name="password"]'); const passwordInput = container.querySelector('input[name="password"]');
fireEvent.blur(passwordInput, { fireEvent.blur(passwordInput, {
@@ -181,7 +179,7 @@ describe('PasswordField', () => {
...props, ...props,
handleErrorChange: jest.fn(), handleErrorChange: jest.fn(),
}; };
const { container } = render(reduxWrapper(<IntlPasswordField {...props} />)); const { container } = render(reduxWrapper(<PasswordField {...props} />));
const passwordInput = container.querySelector('input[name="password"]'); const passwordInput = container.querySelector('input[name="password"]');
fireEvent.blur(passwordInput, { fireEvent.blur(passwordInput, {
@@ -204,7 +202,7 @@ describe('PasswordField', () => {
handleErrorChange: jest.fn(), handleErrorChange: jest.fn(),
}; };
const { getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />)); const { getByLabelText } = render(reduxWrapper(<PasswordField {...props} />));
const passwordIcon = getByLabelText('Show password'); const passwordIcon = getByLabelText('Show password');
@@ -224,7 +222,7 @@ describe('PasswordField', () => {
handleErrorChange: jest.fn(), handleErrorChange: jest.fn(),
}; };
const { getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />)); const { getByLabelText } = render(reduxWrapper(<PasswordField {...props} />));
const passwordIcon = getByLabelText('Show password'); const passwordIcon = getByLabelText('Show password');
@@ -248,7 +246,7 @@ describe('PasswordField', () => {
...props, ...props,
handleErrorChange: jest.fn(), handleErrorChange: jest.fn(),
}; };
const { getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />)); const { getByLabelText } = render(reduxWrapper(<PasswordField {...props} />));
const passwordField = getByLabelText('Password'); const passwordField = getByLabelText('Password');
fireEvent.blur(passwordField, { fireEvent.blur(passwordField, {
target: { target: {
@@ -268,7 +266,7 @@ describe('PasswordField', () => {
handleErrorChange: jest.fn(), handleErrorChange: jest.fn(),
handleBlur: jest.fn(), handleBlur: jest.fn(),
}; };
const { getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />)); const { getByLabelText } = render(reduxWrapper(<PasswordField {...props} />));
const passwordIcon = getByLabelText('Show password'); const passwordIcon = getByLabelText('Show password');

View File

@@ -1,16 +1,35 @@
import React from 'react'; import React from 'react';
import { Provider } from 'react-redux';
import { IntlProvider } from '@edx/frontend-platform/i18n'; import { IntlProvider } from '@edx/frontend-platform/i18n';
import renderer from 'react-test-renderer'; import renderer from 'react-test-renderer';
import configureStore from 'redux-mock-store';
import registerIcons from '../RegisterFaIcons'; import registerIcons from '../RegisterFaIcons';
import SocialAuthProviders from '../SocialAuthProviders'; import SocialAuthProviders from '../SocialAuthProviders';
registerIcons(); registerIcons();
const mockStore = configureStore();
describe('SocialAuthProviders', () => { describe('SocialAuthProviders', () => {
let props = {}; let props = {};
const initialState = {
register: {
registrationFormData: {
configurableFormFields: {
marketingEmailsOptIn: true,
},
},
},
};
const store = mockStore(initialState);
const reduxWrapper = children => (
<IntlProvider locale="en">
<Provider store={store}>{children}</Provider>
</IntlProvider>
);
const appleProvider = { const appleProvider = {
id: 'oa2-apple-id', id: 'oa2-apple-id',
name: 'Apple', name: 'Apple',
@@ -30,11 +49,11 @@ describe('SocialAuthProviders', () => {
it('should match social auth provider with iconImage snapshot', () => { it('should match social auth provider with iconImage snapshot', () => {
props = { socialAuthProviders: [appleProvider, facebookProvider] }; props = { socialAuthProviders: [appleProvider, facebookProvider] };
const tree = renderer.create( const tree = renderer.create(reduxWrapper(
<IntlProvider locale="en"> <IntlProvider locale="en">
<SocialAuthProviders {...props} /> <SocialAuthProviders {...props} />
</IntlProvider>, </IntlProvider>,
).toJSON(); )).toJSON();
expect(tree).toMatchSnapshot(); expect(tree).toMatchSnapshot();
}); });
@@ -48,11 +67,11 @@ describe('SocialAuthProviders', () => {
}], }],
}; };
const tree = renderer.create( const tree = renderer.create(reduxWrapper(
<IntlProvider locale="en"> <IntlProvider locale="en">
<SocialAuthProviders {...props} /> <SocialAuthProviders {...props} />
</IntlProvider>, </IntlProvider>,
).toJSON(); )).toJSON();
expect(tree).toMatchSnapshot(); expect(tree).toMatchSnapshot();
}); });
@@ -66,11 +85,11 @@ describe('SocialAuthProviders', () => {
}], }],
}; };
const tree = renderer.create( const tree = renderer.create(reduxWrapper(
<IntlProvider locale="en"> <IntlProvider locale="en">
<SocialAuthProviders {...props} /> <SocialAuthProviders {...props} />
</IntlProvider>, </IntlProvider>,
).toJSON(); )).toJSON();
expect(tree).toMatchSnapshot(); expect(tree).toMatchSnapshot();
}); });

View File

@@ -5,14 +5,13 @@ import React from 'react';
import { fetchAuthenticatedUser, getAuthenticatedUser } from '@edx/frontend-platform/auth'; import { fetchAuthenticatedUser, getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { render } from '@testing-library/react'; import { render } from '@testing-library/react';
import { act } from 'react-dom/test-utils'; import { act } from 'react-dom/test-utils';
import { UnAuthOnlyRoute } from '..';
import { REGISTER_PAGE } from '../../data/constants';
import { import {
MemoryRouter, Route, BrowserRouter as Router, Routes, MemoryRouter, Route, BrowserRouter as Router, Routes,
} from 'react-router-dom'; } from 'react-router-dom';
import { UnAuthOnlyRoute } from '..';
import { REGISTER_PAGE } from '../../data/constants';
jest.mock('@edx/frontend-platform/auth', () => ({ jest.mock('@edx/frontend-platform/auth', () => ({
getAuthenticatedUser: jest.fn(), getAuthenticatedUser: jest.fn(),
fetchAuthenticatedUser: jest.fn(), fetchAuthenticatedUser: jest.fn(),

View File

@@ -66,14 +66,14 @@ exports[`SocialAuthProviders should match social auth provider with iconClass sn
data-prefix="fab" data-prefix="fab"
focusable="false" focusable="false"
role="img" role="img"
style={Object {}} style={{}}
viewBox="0 0 488 512" viewBox="0 0 488 512"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
> >
<path <path
d="M488 261.8C488 403.3 391.1 504 248 504 110.8 504 0 393.2 0 256S110.8 8 248 8c66.8 0 123 24.5 166.3 64.9l-67.5 64.9C258.5 52.6 94.3 116.6 94.3 256c0 86.5 69.1 156.6 153.7 156.6 98.2 0 135-70.4 140.8-106.9H248v-85.3h236.1c2.3 12.7 3.9 24.9 3.9 41.4z" d="M488 261.8C488 403.3 391.1 504 248 504 110.8 504 0 393.2 0 256S110.8 8 248 8c66.8 0 123 24.5 166.3 64.9l-67.5 64.9C258.5 52.6 94.3 116.6 94.3 256c0 86.5 69.1 156.6 153.7 156.6 98.2 0 135-70.4 140.8-106.9H248v-85.3h236.1c2.3 12.7 3.9 24.9 3.9 41.4z"
fill="currentColor" fill="currentColor"
style={Object {}} style={{}}
/> />
</svg> </svg>
</div> </div>
@@ -93,7 +93,7 @@ exports[`SocialAuthProviders should match social auth provider with iconClass sn
`; `;
exports[`SocialAuthProviders should match social auth provider with iconImage snapshot 1`] = ` exports[`SocialAuthProviders should match social auth provider with iconImage snapshot 1`] = `
Array [ [
<button <button
className="btn-social btn-oa2-apple-id mr-3" className="btn-social btn-oa2-apple-id mr-3"
data-provider-url="/auth/login/apple-id/?auth_entry=login&next=/dashboard" data-provider-url="/auth/login/apple-id/?auth_entry=login&next=/dashboard"

View File

@@ -21,7 +21,7 @@ exports[`ThirdPartyAuthAlert should match login page third party auth alert mess
`; `;
exports[`ThirdPartyAuthAlert should match register page third party auth alert message snapshot 1`] = ` exports[`ThirdPartyAuthAlert should match register page third party auth alert message snapshot 1`] = `
Array [ [
<div <div
className="fade alert-content alert-success mt-n2 mb-5 alert show" className="fade alert-content alert-success mt-n2 mb-5 alert show"
id="tpa-alert" id="tpa-alert"

View File

@@ -5,23 +5,23 @@ exports[`Zendesk Help should match login page third party auth alert message sna
cookies={true} cookies={true}
defer={true} defer={true}
webWidget={ webWidget={
Object { {
"answerBot": Object { "answerBot": {
"avatar": Object { "avatar": {
"name": Object { "name": {
"*": "edX Support", "*": "edX Support",
}, },
"url": undefined, "url": undefined,
}, },
"contactOnlyAfterQuery": true, "contactOnlyAfterQuery": true,
"suppress": false, "suppress": false,
"title": Object { "title": {
"*": "edX Support", "*": "edX Support",
}, },
}, },
"chat": Object { "chat": {
"departments": Object { "departments": {
"enabled": Array [ "enabled": [
"account settings", "account settings",
"billing and payments", "billing and payments",
"certificates", "certificates",
@@ -33,17 +33,17 @@ exports[`Zendesk Help should match login page third party auth alert message sna
}, },
"suppress": false, "suppress": false,
}, },
"contactForm": Object { "contactForm": {
"attachments": true, "attachments": true,
"selectTicketForm": Object { "selectTicketForm": {
"*": "Please choose your request type:", "*": "Please choose your request type:",
}, },
"ticketForms": Array [ "ticketForms": [
Object { {
"fields": Array [ "fields": [
Object { {
"id": "description", "id": "description",
"prefill": Object { "prefill": {
"*": "", "*": "",
}, },
}, },
@@ -53,10 +53,10 @@ exports[`Zendesk Help should match login page third party auth alert message sna
}, },
], ],
}, },
"contactOptions": Object { "contactOptions": {
"enabled": false, "enabled": false,
}, },
"helpCenter": Object { "helpCenter": {
"originalArticleButton": true, "originalArticleButton": true,
}, },
} }

View File

@@ -4,12 +4,14 @@ const configuration = {
USER_RETENTION_COOKIE_NAME: process.env.USER_RETENTION_COOKIE_NAME || '', USER_RETENTION_COOKIE_NAME: process.env.USER_RETENTION_COOKIE_NAME || '',
// Features // Features
DISABLE_ENTERPRISE_LOGIN: process.env.DISABLE_ENTERPRISE_LOGIN || '', DISABLE_ENTERPRISE_LOGIN: process.env.DISABLE_ENTERPRISE_LOGIN || '',
ENABLE_AUTO_GENERATED_USERNAME: process.env.ENABLE_AUTO_GENERATED_USERNAME || false,
ENABLE_DYNAMIC_REGISTRATION_FIELDS: process.env.ENABLE_DYNAMIC_REGISTRATION_FIELDS || false, ENABLE_DYNAMIC_REGISTRATION_FIELDS: process.env.ENABLE_DYNAMIC_REGISTRATION_FIELDS || false,
ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN: process.env.ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN || false, ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN: process.env.ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN || false,
ENABLE_POST_REGISTRATION_RECOMMENDATIONS: process.env.ENABLE_POST_REGISTRATION_RECOMMENDATIONS || false, ENABLE_POST_REGISTRATION_RECOMMENDATIONS: process.env.ENABLE_POST_REGISTRATION_RECOMMENDATIONS || false,
MARKETING_EMAILS_OPT_IN: process.env.MARKETING_EMAILS_OPT_IN || '', MARKETING_EMAILS_OPT_IN: process.env.MARKETING_EMAILS_OPT_IN || '',
SHOW_CONFIGURABLE_EDX_FIELDS: process.env.SHOW_CONFIGURABLE_EDX_FIELDS || false, SHOW_CONFIGURABLE_EDX_FIELDS: process.env.SHOW_CONFIGURABLE_EDX_FIELDS || false,
SHOW_REGISTRATION_LINKS: process.env.SHOW_REGISTRATION_LINKS !== 'false', SHOW_REGISTRATION_LINKS: process.env.SHOW_REGISTRATION_LINKS !== 'false',
ENABLE_HARD_CODE_OPTIONAL_FIELDS: process.env.ENABLE_HARD_CODE_OPTIONAL_FIELDS || false,
ENABLE_IMAGE_LAYOUT: process.env.ENABLE_IMAGE_LAYOUT || false, ENABLE_IMAGE_LAYOUT: process.env.ENABLE_IMAGE_LAYOUT || false,
// Links // Links
ACTIVATION_EMAIL_SUPPORT_LINK: process.env.ACTIVATION_EMAIL_SUPPORT_LINK || null, ACTIVATION_EMAIL_SUPPORT_LINK: process.env.ACTIVATION_EMAIL_SUPPORT_LINK || null,
@@ -34,6 +36,8 @@ const configuration = {
ZENDESK_LOGO_URL: process.env.ZENDESK_LOGO_URL, ZENDESK_LOGO_URL: process.env.ZENDESK_LOGO_URL,
ALGOLIA_APP_ID: process.env.ALGOLIA_APP_ID || '', ALGOLIA_APP_ID: process.env.ALGOLIA_APP_ID || '',
ALGOLIA_SEARCH_API_KEY: process.env.ALGOLIA_SEARCH_API_KEY || '', ALGOLIA_SEARCH_API_KEY: process.env.ALGOLIA_SEARCH_API_KEY || '',
AUTO_GENERATED_USERNAME_EXPERIMENT_ID: process.env.AUTO_GENERATED_USERNAME_EXPERIMENT_ID || '',
RECAPTCHA_SITE_KEY_WEB: process.env.RECAPTCHA_SITE_KEY_WEB || '',
}; };
export default configuration; export default configuration;

View File

@@ -37,3 +37,4 @@ export const VALID_EMAIL_REGEX = '(^[-!#$%&\'*+/=?^_`{}|~0-9A-Z]+(\\.[-!#$%&\'*+
// things like auto-enrollment upon login and registration. // things like auto-enrollment upon login and registration.
export const AUTH_PARAMS = ['course_id', 'enrollment_action', 'course_mode', 'email_opt_in', 'purchase_workflow', 'next', 'register_for_free', 'track', 'is_account_recovery', 'variant', 'host', 'cta']; export const AUTH_PARAMS = ['course_id', 'enrollment_action', 'course_mode', 'email_opt_in', 'purchase_workflow', 'next', 'register_for_free', 'track', 'is_account_recovery', 'variant', 'host', 'cta'];
export const REDIRECT = 'redirect'; export const REDIRECT = 'redirect';
export const APP_NAME = 'authn_mfe';

View File

@@ -1,5 +1,6 @@
import { combineReducers } from 'redux'; import { combineReducers } from 'redux';
import { reducer as cohesionReducer, storeName as cohesionStoreName } from '../cohesion/data/reducers';
import { import {
reducer as commonComponentsReducer, reducer as commonComponentsReducer,
storeName as commonComponentsStoreName, storeName as commonComponentsStoreName,
@@ -31,6 +32,7 @@ const createRootReducer = () => combineReducers({
[commonComponentsStoreName]: commonComponentsReducer, [commonComponentsStoreName]: commonComponentsReducer,
[forgotPasswordStoreName]: forgotPasswordReducer, [forgotPasswordStoreName]: forgotPasswordReducer,
[resetPasswordStoreName]: resetPasswordReducer, [resetPasswordStoreName]: resetPasswordReducer,
[cohesionStoreName]: cohesionReducer,
[authnProgressiveProfilingStoreName]: authnProgressiveProfilingReducers, [authnProgressiveProfilingStoreName]: authnProgressiveProfilingReducers,
}); });
export default createRootReducer; export default createRootReducer;

37
src/data/segment/utils.js Normal file
View File

@@ -0,0 +1,37 @@
/* eslint-disable import/prefer-default-export */
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
import { APP_NAME } from '../constants';
export const LINK_TIMEOUT = 300;
/**
* Creates an event tracker function that sends a tracking event with the given name and options.
*
* @param {string} name - The name of the event to be tracked.
* @param {object} [options={}] - Additional options to be included with the event.
* @returns {function} - A function that, when called, sends the tracking event.
*/
export const createEventTracker = (name, options = {}) => () => sendTrackEvent(
name,
{ ...options, app_name: APP_NAME },
);
/**
* Creates an event tracker function that sends a tracking event with the given name and options.
*
* @param {string} name - The name of the event to be tracked.
* @param {object} [options={}] - Additional options to be included with the event.
* @returns {function} - A function that, when called, sends the tracking event.
*/
export const createPageEventTracker = (name, options = null) => () => sendPageEvent(
name,
options,
{ app_name: APP_NAME },
);
export const createLinkTracker = (tracker, href) => (e) => {
e.preventDefault();
tracker();
return setTimeout(() => { window.location.href = href; }, LINK_TIMEOUT);
};

View File

@@ -11,3 +11,11 @@ export default function setCookie(cookieName, cookieValue, cookieExpiry) {
cookies.set(cookieName, cookieValue, options); cookies.set(cookieName, cookieValue, options);
} }
} }
export function removeCookie(cookieName) {
if (cookieName) {
const cookies = new Cookies();
const options = { domain: getConfig().SESSION_COOKIE_DOMAIN, path: '/' };
cookies.remove(cookieName, options);
}
}

View File

@@ -81,3 +81,9 @@ export const isHostAvailableInQueryParams = () => {
const queryParams = getAllPossibleQueryParams(); const queryParams = getAllPossibleQueryParams();
return 'host' in queryParams; return 'host' in queryParams;
}; };
export const redirectWithDelay = (redirectUrl) => {
setTimeout(() => {
window.location.href = redirectUrl;
}, 1000);
};

View File

@@ -8,4 +8,4 @@ export {
windowScrollTo, windowScrollTo,
} from './dataUtils'; } from './dataUtils';
export { default as AsyncActionType } from './reduxUtils'; export { default as AsyncActionType } from './reduxUtils';
export { default as setCookie } from './cookies'; export { default as setCookie, removeCookie } from './cookies';

View File

@@ -2,7 +2,6 @@ import React, { useEffect, useState } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { getConfig } from '@edx/frontend-platform'; import { getConfig } from '@edx/frontend-platform';
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
import { useIntl } from '@edx/frontend-platform/i18n'; import { useIntl } from '@edx/frontend-platform/i18n';
import { import {
Form, Form,
@@ -25,6 +24,10 @@ import BaseContainer from '../base-container';
import { FormGroup } from '../common-components'; import { FormGroup } from '../common-components';
import { DEFAULT_STATE, LOGIN_PAGE, VALID_EMAIL_REGEX } from '../data/constants'; import { DEFAULT_STATE, LOGIN_PAGE, VALID_EMAIL_REGEX } from '../data/constants';
import { updatePathWithQueryParams, windowScrollTo } from '../data/utils'; import { updatePathWithQueryParams, windowScrollTo } from '../data/utils';
import {
trackForgotPasswordPageEvent,
trackForgotPasswordPageViewed,
} from '../tracking/trackers/forgotpassword';
const ForgotPasswordPage = (props) => { const ForgotPasswordPage = (props) => {
const platformName = getConfig().SITE_NAME; const platformName = getConfig().SITE_NAME;
@@ -41,8 +44,8 @@ const ForgotPasswordPage = (props) => {
const navigate = useNavigate(); const navigate = useNavigate();
useEffect(() => { useEffect(() => {
sendPageEvent('login_and_registration', 'reset'); trackForgotPasswordPageEvent();
sendTrackEvent('edx.bi.password_reset_form.viewed', { category: 'user-engagement' }); trackForgotPasswordPageViewed();
}, []); }, []);
useEffect(() => { useEffect(() => {

View File

@@ -1,8 +1,7 @@
import React from 'react';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { mergeConfig } from '@edx/frontend-platform'; import { mergeConfig } from '@edx/frontend-platform';
import { configure, injectIntl, IntlProvider } from '@edx/frontend-platform/i18n'; import { configure, IntlProvider } from '@edx/frontend-platform/i18n';
import { import {
fireEvent, render, screen, fireEvent, render, screen,
} from '@testing-library/react'; } from '@testing-library/react';
@@ -26,7 +25,6 @@ jest.mock('react-router-dom', () => ({
useNavigate: () => mockedNavigator, useNavigate: () => mockedNavigator,
})); }));
const IntlForgotPasswordPage = injectIntl(ForgotPasswordPage);
const mockStore = configureStore(); const mockStore = configureStore();
const initialState = { const initialState = {
@@ -78,7 +76,7 @@ describe('ForgotPasswordPage', () => {
); );
it('not should display need other help signing in button', () => { it('not should display need other help signing in button', () => {
const { queryByTestId } = render(reduxWrapper(<IntlForgotPasswordPage {...props} />)); const { queryByTestId } = render(reduxWrapper(<ForgotPasswordPage {...props} />));
const forgotPasswordButton = queryByTestId('forgot-password'); const forgotPasswordButton = queryByTestId('forgot-password');
expect(forgotPasswordButton).toBeNull(); expect(forgotPasswordButton).toBeNull();
}); });
@@ -87,14 +85,14 @@ describe('ForgotPasswordPage', () => {
mergeConfig({ mergeConfig({
LOGIN_ISSUE_SUPPORT_LINK: '/support', LOGIN_ISSUE_SUPPORT_LINK: '/support',
}); });
render(reduxWrapper(<IntlForgotPasswordPage {...props} />)); render(reduxWrapper(<ForgotPasswordPage {...props} />));
const forgotPasswordButton = screen.findByText('Need help signing in?'); const forgotPasswordButton = screen.findByText('Need help signing in?');
expect(forgotPasswordButton).toBeDefined(); expect(forgotPasswordButton).toBeDefined();
}); });
it('should display email validation error message', async () => { it('should display email validation error message', async () => {
const validationMessage = 'We were unable to contact you.Enter a valid email address below.'; const validationMessage = 'We were unable to contact you.Enter a valid email address below.';
const { container } = render(reduxWrapper(<IntlForgotPasswordPage {...props} />)); const { container } = render(reduxWrapper(<ForgotPasswordPage {...props} />));
const emailInput = screen.getByLabelText('Email'); const emailInput = screen.getByLabelText('Email');
@@ -115,7 +113,7 @@ describe('ForgotPasswordPage', () => {
const expectedMessage = 'We were unable to contact you.' const expectedMessage = 'We were unable to contact you.'
+ 'An error has occurred. Try refreshing the page, or check your internet connection.'; + 'An error has occurred. Try refreshing the page, or check your internet connection.';
const { container } = render(reduxWrapper(<IntlForgotPasswordPage {...props} />)); const { container } = render(reduxWrapper(<ForgotPasswordPage {...props} />));
const alertElements = container.querySelectorAll('.alert-danger'); const alertElements = container.querySelectorAll('.alert-danger');
const validationErrors = alertElements[0].textContent; const validationErrors = alertElements[0].textContent;
@@ -124,7 +122,7 @@ describe('ForgotPasswordPage', () => {
it('should display empty email validation message', async () => { it('should display empty email validation message', async () => {
const validationMessage = 'We were unable to contact you.Enter your email below.'; const validationMessage = 'We were unable to contact you.Enter your email below.';
const { container } = render(reduxWrapper(<IntlForgotPasswordPage {...props} />)); const { container } = render(reduxWrapper(<ForgotPasswordPage {...props} />));
const submitButton = screen.getByText('Submit'); const submitButton = screen.getByText('Submit');
fireEvent.click(submitButton); fireEvent.click(submitButton);
@@ -141,7 +139,7 @@ describe('ForgotPasswordPage', () => {
forgotPassword: { status: 'forbidden' }, forgotPassword: { status: 'forbidden' },
}); });
const { container } = render(reduxWrapper(<IntlForgotPasswordPage {...props} />)); const { container } = render(reduxWrapper(<ForgotPasswordPage {...props} />));
const alertElements = container.querySelectorAll('.alert-danger'); const alertElements = container.querySelectorAll('.alert-danger');
const validationErrors = alertElements[0].textContent; const validationErrors = alertElements[0].textContent;
@@ -149,7 +147,7 @@ describe('ForgotPasswordPage', () => {
}); });
it('should not display any error message on change event', () => { it('should not display any error message on change event', () => {
render(reduxWrapper(<IntlForgotPasswordPage {...props} />)); render(reduxWrapper(<ForgotPasswordPage {...props} />));
const emailInput = screen.getByLabelText('Email'); const emailInput = screen.getByLabelText('Email');
@@ -172,7 +170,7 @@ describe('ForgotPasswordPage', () => {
}; };
store.dispatch = jest.fn(store.dispatch); store.dispatch = jest.fn(store.dispatch);
render(reduxWrapper(<IntlForgotPasswordPage {...props} />)); render(reduxWrapper(<ForgotPasswordPage {...props} />));
const emailInput = screen.getByLabelText('Email'); const emailInput = screen.getByLabelText('Email');
fireEvent.blur(emailInput); fireEvent.blur(emailInput);
@@ -187,7 +185,7 @@ describe('ForgotPasswordPage', () => {
emailValidationError: validationMessage, emailValidationError: validationMessage,
email: '', email: '',
}; };
const { container } = render(reduxWrapper(<IntlForgotPasswordPage {...props} />)); const { container } = render(reduxWrapper(<ForgotPasswordPage {...props} />));
const validationElement = container.querySelector('.pgn__form-text-invalid'); const validationElement = container.querySelector('.pgn__form-text-invalid');
expect(validationElement.textContent).toEqual(validationMessage); expect(validationElement.textContent).toEqual(validationMessage);
}); });
@@ -205,7 +203,7 @@ describe('ForgotPasswordPage', () => {
store.dispatch = jest.fn(store.dispatch); store.dispatch = jest.fn(store.dispatch);
render(reduxWrapper(<IntlForgotPasswordPage {...props} />)); render(reduxWrapper(<ForgotPasswordPage {...props} />));
const emailInput = screen.getByLabelText('Email'); const emailInput = screen.getByLabelText('Email');
fireEvent.focus(emailInput); fireEvent.focus(emailInput);
@@ -219,7 +217,7 @@ describe('ForgotPasswordPage', () => {
emailValidationError: '', emailValidationError: '',
email: '', email: '',
}; };
render(reduxWrapper(<IntlForgotPasswordPage {...props} />)); render(reduxWrapper(<ForgotPasswordPage {...props} />));
const errorElement = screen.queryByTestId('email-invalid-feedback'); const errorElement = screen.queryByTestId('email-invalid-feedback');
expect(errorElement).toBeNull(); expect(errorElement).toBeNull();
}); });
@@ -236,7 +234,7 @@ describe('ForgotPasswordPage', () => {
+ 'receive a password reset message after 1 minute, verify that you entered the correct email address,' + 'receive a password reset message after 1 minute, verify that you entered the correct email address,'
+ ' or check your spam folder. If you need further assistance, contact technical support.'; + ' or check your spam folder. If you need further assistance, contact technical support.';
const { container } = render(reduxWrapper(<IntlForgotPasswordPage {...props} />)); const { container } = render(reduxWrapper(<ForgotPasswordPage {...props} />));
const successElement = findByTextContent(container, successMessage); const successElement = findByTextContent(container, successMessage);
expect(successElement).toBeDefined(); expect(successElement).toBeDefined();
@@ -254,7 +252,7 @@ describe('ForgotPasswordPage', () => {
+ 'This password reset link is invalid. It may have been used already. ' + 'This password reset link is invalid. It may have been used already. '
+ 'Enter your email below to receive a new link.'; + 'Enter your email below to receive a new link.';
const { container } = render(reduxWrapper(<IntlForgotPasswordPage {...props} />)); const { container } = render(reduxWrapper(<ForgotPasswordPage {...props} />));
const successElement = findByTextContent(container, successMessage); const successElement = findByTextContent(container, successMessage);
expect(successElement).toBeDefined(); expect(successElement).toBeDefined();
@@ -262,7 +260,7 @@ describe('ForgotPasswordPage', () => {
}); });
it('should redirect onto login page', async () => { it('should redirect onto login page', async () => {
const { container } = render(reduxWrapper(<IntlForgotPasswordPage {...props} />)); const { container } = render(reduxWrapper(<ForgotPasswordPage {...props} />));
const navElement = container.querySelector('nav'); const navElement = container.querySelector('nav');
const anchorElement = navElement.querySelector('a'); const anchorElement = navElement.querySelector('a');

View File

@@ -1,27 +1,36 @@
import 'core-js/stable'; import 'core-js/stable';
import 'regenerator-runtime/runtime'; import 'regenerator-runtime/runtime';
import React from 'react'; import React, { StrictMode } from 'react';
import ReactDOM from 'react-dom';
import { import {
APP_INIT_ERROR, APP_READY, initialize, mergeConfig, subscribe, APP_INIT_ERROR, APP_READY, initialize, mergeConfig, subscribe,
} from '@edx/frontend-platform'; } from '@edx/frontend-platform';
import { ErrorPage } from '@edx/frontend-platform/react'; import { ErrorPage } from '@edx/frontend-platform/react';
import { createRoot } from 'react-dom/client';
import configuration from './config'; import configuration from './config';
import messages from './i18n'; import messages from './i18n';
import MainApp from './MainApp'; import MainApp from './MainApp';
subscribe(APP_READY, () => { subscribe(APP_READY, () => {
ReactDOM.render( const root = createRoot(document.getElementById('root'));
<MainApp />,
document.getElementById('root'), root.render(
<StrictMode>
<MainApp />
</StrictMode>,
); );
}); });
subscribe(APP_INIT_ERROR, (error) => { subscribe(APP_INIT_ERROR, (error) => {
ReactDOM.render(<ErrorPage message={error.message} />, document.getElementById('root')); const root = createRoot(document.getElementById('root'));
root.render(
<StrictMode>
<ErrorPage message={error.message} />
</StrictMode>,
);
}); });
initialize({ initialize({

View File

@@ -1,6 +1,2 @@
@import "~@edx/brand/paragon/fonts"; @use "@openedx/paragon/styles/css/core/custom-media-breakpoints" as paragonCustomMediaBreakpoints;
@import "~@edx/brand/paragon/variables";
@import "~@openedx/paragon/scss/core/core";
@import "~@edx/brand/paragon/overrides";
@import "sass/style"; @import "sass/style";

View File

@@ -2,7 +2,7 @@ import React from 'react';
import { getConfig } from '@edx/frontend-platform'; import { getConfig } from '@edx/frontend-platform';
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n'; import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
import { Alert } from '@openedx/paragon'; import { Alert, Hyperlink } from '@openedx/paragon';
import { CheckCircle, Error } from '@openedx/paragon/icons'; import { CheckCircle, Error } from '@openedx/paragon/icons';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
@@ -36,19 +36,17 @@ const AccountActivationMessage = ({ messageType }) => {
break; break;
} }
case ACCOUNT_ACTIVATION_MESSAGE.ERROR: { case ACCOUNT_ACTIVATION_MESSAGE.ERROR: {
const supportLink = ( const supportEmail = (
<Alert.Link href={getConfig().ACTIVATION_EMAIL_SUPPORT_LINK}> <Hyperlink isInline destination={`mailto:${getConfig().INFO_EMAIL}`}>{getConfig().INFO_EMAIL}</Hyperlink>
{formatMessage(messages['account.activation.support.link'])}
</Alert.Link>
); );
heading = formatMessage(messages[`account.${activationOrConfirmation}.error.message.title`]); heading = formatMessage(messages[`account.${activationOrConfirmation}.error.message.title`]);
activationMessage = ( activationMessage = (
<FormattedMessage <FormattedMessage
id="account.activation.error.message" id="account.activation.error.message"
defaultMessage="Something went wrong, please {supportLink} to resolve this issue." defaultMessage="Something went wrong, please contact {supportEmail} to resolve this issue."
description="Account activation error message" description="Account activation error message"
values={{ supportLink }} values={{ supportEmail }}
/> />
); );
break; break;

View File

@@ -1,4 +1,5 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import { getConfig } from '@edx/frontend-platform'; import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n'; import { useIntl } from '@edx/frontend-platform/i18n';
@@ -10,19 +11,23 @@ import PropTypes from 'prop-types';
import { Link, useNavigate } from 'react-router-dom'; import { Link, useNavigate } from 'react-router-dom';
import messages from './messages'; import messages from './messages';
import trackCohesionEvent from '../cohesion/trackers';
import { DEFAULT_REDIRECT_URL, RESET_PAGE } from '../data/constants'; import { DEFAULT_REDIRECT_URL, RESET_PAGE } from '../data/constants';
import { updatePathWithQueryParams } from '../data/utils'; import { updatePathWithQueryParams } from '../data/utils';
import { redirectWithDelay } from '../data/utils/dataUtils';
import useMobileResponsive from '../data/utils/useMobileResponsive'; import useMobileResponsive from '../data/utils/useMobileResponsive';
const ChangePasswordPrompt = ({ variant, redirectUrl }) => { const ChangePasswordPrompt = ({ variant, redirectUrl }) => {
const isMobileView = useMobileResponsive(); const isMobileView = useMobileResponsive();
const [redirectToResetPasswordPage, setRedirectToResetPasswordPage] = useState(false); const [redirectToResetPasswordPage, setRedirectToResetPasswordPage] = useState(false);
const cohesionEventData = useSelector(state => state.cohesion.eventData);
const handlers = { const handlers = {
handleToggleOff: () => { handleToggleOff: () => {
if (variant === 'block') { if (variant === 'block') {
setRedirectToResetPasswordPage(true); setRedirectToResetPasswordPage(true);
} else { } else {
window.location.href = redirectUrl || getConfig().LMS_BASE_URL.concat(DEFAULT_REDIRECT_URL); trackCohesionEvent(cohesionEventData);
redirectWithDelay(redirectUrl || getConfig().LMS_BASE_URL.concat(DEFAULT_REDIRECT_URL));
} }
}, },
}; };

View File

@@ -1,9 +1,8 @@
import React, { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { connect } from 'react-redux'; import { connect, useDispatch } from 'react-redux';
import { getConfig } from '@edx/frontend-platform'; import { getConfig } from '@edx/frontend-platform';
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics'; import { useIntl } from '@edx/frontend-platform/i18n';
import { injectIntl, useIntl } from '@edx/frontend-platform/i18n';
import { import {
Form, StatefulButton, Form, StatefulButton,
} from '@openedx/paragon'; } from '@openedx/paragon';
@@ -21,6 +20,10 @@ import {
import { INVALID_FORM, TPA_AUTHENTICATION_FAILURE } from './data/constants'; import { INVALID_FORM, TPA_AUTHENTICATION_FAILURE } from './data/constants';
import LoginFailureMessage from './LoginFailure'; import LoginFailureMessage from './LoginFailure';
import messages from './messages'; import messages from './messages';
import {
ELEMENT_NAME, ELEMENT_TEXT, ELEMENT_TYPES, PAGE_TYPES,
} from '../cohesion/constants';
import { setCohesionEventStates } from '../cohesion/data/actions';
import { import {
FormGroup, FormGroup,
InstitutionLogistration, InstitutionLogistration,
@@ -32,9 +35,7 @@ import { getThirdPartyAuthContext } from '../common-components/data/actions';
import { thirdPartyAuthContextSelector } from '../common-components/data/selectors'; import { thirdPartyAuthContextSelector } from '../common-components/data/selectors';
import EnterpriseSSO from '../common-components/EnterpriseSSO'; import EnterpriseSSO from '../common-components/EnterpriseSSO';
import ThirdPartyAuth from '../common-components/ThirdPartyAuth'; import ThirdPartyAuth from '../common-components/ThirdPartyAuth';
import { import { DEFAULT_STATE, PENDING_STATE, RESET_PAGE } from '../data/constants';
DEFAULT_STATE, PENDING_STATE, RESET_PAGE,
} from '../data/constants';
import { import {
getActivationStatus, getActivationStatus,
getAllPossibleQueryParams, getAllPossibleQueryParams,
@@ -42,7 +43,11 @@ import {
getTpaProvider, getTpaProvider,
updatePathWithQueryParams, updatePathWithQueryParams,
} from '../data/utils'; } from '../data/utils';
import { removeCookie } from '../data/utils/cookies';
import ResetPasswordSuccess from '../reset-password/ResetPasswordSuccess'; import ResetPasswordSuccess from '../reset-password/ResetPasswordSuccess';
import {
trackForgotPasswordLinkClick, trackLoginPageViewed, trackLoginSuccess,
} from '../tracking/trackers/login';
const LoginPage = (props) => { const LoginPage = (props) => {
const { const {
@@ -69,6 +74,7 @@ const LoginPage = (props) => {
getTPADataFromBackend, getTPADataFromBackend,
} = props; } = props;
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const dispatch = useDispatch();
const activationMsgType = getActivationStatus(); const activationMsgType = getActivationStatus();
const queryParams = useMemo(() => getAllPossibleQueryParams(), []); const queryParams = useMemo(() => getAllPossibleQueryParams(), []);
@@ -78,9 +84,17 @@ const LoginPage = (props) => {
const tpaHint = getTpaHint(); const tpaHint = getTpaHint();
useEffect(() => { useEffect(() => {
sendPageEvent('login_and_registration', 'login'); trackLoginPageViewed();
}, []); }, []);
useEffect(() => {
if (loginResult.success) {
trackLoginSuccess();
// Remove this cookie that was set to capture marketingEmailsOptIn for the onboarding component
removeCookie('ssoPipelineRedirectionDone');
}
}, [loginResult]);
useEffect(() => { useEffect(() => {
const payload = { ...queryParams }; const payload = { ...queryParams };
if (tpaHint) { if (tpaHint) {
@@ -140,6 +154,15 @@ const LoginPage = (props) => {
const handleSubmit = (event) => { const handleSubmit = (event) => {
event.preventDefault(); event.preventDefault();
const eventData = {
pageType: PAGE_TYPES.SIGN_IN,
elementType: ELEMENT_TYPES.BUTTON,
webElementText: ELEMENT_TEXT.SIGN_IN,
webElementName: ELEMENT_NAME.SIGN_IN,
};
dispatch(setCohesionEventStates(eventData));
if (showResetPasswordSuccessBanner) { if (showResetPasswordSuccessBanner) {
props.dismissPasswordResetBanner(); props.dismissPasswordResetBanner();
} }
@@ -170,9 +193,6 @@ const LoginPage = (props) => {
const { name } = event.target; const { name } = event.target;
setErrors(prevErrors => ({ ...prevErrors, [name]: '' })); setErrors(prevErrors => ({ ...prevErrors, [name]: '' }));
}; };
const trackForgotPasswordLinkClick = () => {
sendTrackEvent('edx.bi.password-reset_form.toggled', { category: 'user-engagement' });
};
const { provider, skipHintedLogin } = getTpaProvider(tpaHint, providers, secondaryProviders); const { provider, skipHintedLogin } = getTpaProvider(tpaHint, providers, secondaryProviders);
@@ -208,6 +228,7 @@ const LoginPage = (props) => {
success={loginResult.success} success={loginResult.success}
redirectUrl={loginResult.redirectUrl} redirectUrl={loginResult.redirectUrl}
finishAuthUrl={finishAuthUrl} finishAuthUrl={finishAuthUrl}
currentProvider={currentProvider}
/> />
<div className="mw-xs mt-3 mb-2"> <div className="mw-xs mt-3 mb-2">
<LoginFailureMessage <LoginFailureMessage
@@ -365,4 +386,4 @@ export default connect(
loginRequest, loginRequest,
getTPADataFromBackend: getThirdPartyAuthContext, getTPADataFromBackend: getThirdPartyAuthContext,
}, },
)(injectIntl(LoginPage)); )(LoginPage);

View File

@@ -95,11 +95,6 @@ const messages = defineMessages({
defaultMessage: 'Your account could not be activated', defaultMessage: 'Your account could not be activated',
description: 'Account Activation error message title', description: 'Account Activation error message title',
}, },
'account.activation.support.link': {
id: 'account.activation.support.link',
defaultMessage: 'contact support',
description: 'Link text used in account activation error message to go to learner help center',
},
// Email Confirmation Strings // Email Confirmation Strings
'account.confirmation.success.message.title': { 'account.confirmation.success.message.title': {
id: 'account.confirmation.success.message.title', id: 'account.confirmation.success.message.title',

View File

@@ -1,7 +1,5 @@
import React from 'react';
import { mergeConfig } from '@edx/frontend-platform'; import { mergeConfig } from '@edx/frontend-platform';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n'; import { IntlProvider } from '@edx/frontend-platform/i18n';
import { import {
render, screen, render, screen,
} from '@testing-library/react'; } from '@testing-library/react';
@@ -9,8 +7,6 @@ import {
import AccountActivationMessage from '../AccountActivationMessage'; import AccountActivationMessage from '../AccountActivationMessage';
import { ACCOUNT_ACTIVATION_MESSAGE } from '../data/constants'; import { ACCOUNT_ACTIVATION_MESSAGE } from '../data/constants';
const IntlAccountActivationMessage = injectIntl(AccountActivationMessage);
describe('AccountActivationMessage', () => { describe('AccountActivationMessage', () => {
beforeEach(() => { beforeEach(() => {
mergeConfig({ mergeConfig({
@@ -21,7 +17,7 @@ describe('AccountActivationMessage', () => {
it('should match account already activated message', () => { it('should match account already activated message', () => {
render( render(
<IntlProvider locale="en"> <IntlProvider locale="en">
<IntlAccountActivationMessage messageType={ACCOUNT_ACTIVATION_MESSAGE.INFO} /> <AccountActivationMessage messageType={ACCOUNT_ACTIVATION_MESSAGE.INFO} />
</IntlProvider>, </IntlProvider>,
); );
@@ -36,7 +32,7 @@ describe('AccountActivationMessage', () => {
it('should match account activated success message', () => { it('should match account activated success message', () => {
render( render(
<IntlProvider locale="en"> <IntlProvider locale="en">
<IntlAccountActivationMessage messageType={ACCOUNT_ACTIVATION_MESSAGE.SUCCESS} /> <AccountActivationMessage messageType={ACCOUNT_ACTIVATION_MESSAGE.SUCCESS} />
</IntlProvider>, </IntlProvider>,
); );
@@ -53,12 +49,12 @@ describe('AccountActivationMessage', () => {
it('should match account activation error message', () => { it('should match account activation error message', () => {
render( render(
<IntlProvider locale="en"> <IntlProvider locale="en">
<IntlAccountActivationMessage messageType={ACCOUNT_ACTIVATION_MESSAGE.ERROR} /> <AccountActivationMessage messageType={ACCOUNT_ACTIVATION_MESSAGE.ERROR} />
</IntlProvider>, </IntlProvider>,
); );
const expectedMessage = 'Your account could not be activated' const expectedMessage = 'Your account could not be activated'
+ 'Something went wrong, please contact support to resolve this issue.'; + 'Something went wrong, please contact to resolve this issue.';
expect(screen.getByText( expect(screen.getByText(
'', '',
@@ -69,7 +65,7 @@ describe('AccountActivationMessage', () => {
it('should not display anything for invalid message type', () => { it('should not display anything for invalid message type', () => {
const { container } = render( const { container } = render(
<IntlProvider locale="en"> <IntlProvider locale="en">
<IntlAccountActivationMessage messageType="invalid-message" /> <AccountActivationMessage messageType="invalid-message" />
</IntlProvider>, </IntlProvider>,
); );
@@ -88,7 +84,7 @@ describe('EmailConfirmationMessage', () => {
it('should match email already confirmed message', () => { it('should match email already confirmed message', () => {
render( render(
<IntlProvider locale="en"> <IntlProvider locale="en">
<IntlAccountActivationMessage messageType={ACCOUNT_ACTIVATION_MESSAGE.INFO} /> <AccountActivationMessage messageType={ACCOUNT_ACTIVATION_MESSAGE.INFO} />
</IntlProvider>, </IntlProvider>,
); );
@@ -103,7 +99,7 @@ describe('EmailConfirmationMessage', () => {
it('should match email confirmation success message', () => { it('should match email confirmation success message', () => {
render( render(
<IntlProvider locale="en"> <IntlProvider locale="en">
<IntlAccountActivationMessage messageType={ACCOUNT_ACTIVATION_MESSAGE.SUCCESS} /> <AccountActivationMessage messageType={ACCOUNT_ACTIVATION_MESSAGE.SUCCESS} />
</IntlProvider>, </IntlProvider>,
); );
const expectedMessage = 'Success! You have confirmed your email.Sign in to continue.'; const expectedMessage = 'Success! You have confirmed your email.Sign in to continue.';
@@ -117,11 +113,11 @@ describe('EmailConfirmationMessage', () => {
it('should match email confirmation error message', () => { it('should match email confirmation error message', () => {
render( render(
<IntlProvider locale="en"> <IntlProvider locale="en">
<IntlAccountActivationMessage messageType={ACCOUNT_ACTIVATION_MESSAGE.ERROR} /> <AccountActivationMessage messageType={ACCOUNT_ACTIVATION_MESSAGE.ERROR} />
</IntlProvider>, </IntlProvider>,
); );
const expectedMessage = 'Your email could not be confirmed' const expectedMessage = 'Your email could not be confirmed'
+ 'Something went wrong, please contact support to resolve this issue.'; + 'Something went wrong, please contact to resolve this issue.';
expect(screen.getByText( expect(screen.getByText(
'', '',
{ selector: '#account-activation-message' }, { selector: '#account-activation-message' },

View File

@@ -1,18 +1,29 @@
import React from 'react'; import React from 'react';
import { Provider } from 'react-redux';
import { getConfig } from '@edx/frontend-platform'; import { getConfig } from '@edx/frontend-platform';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n'; import { IntlProvider } from '@edx/frontend-platform/i18n';
import { import {
fireEvent, render, screen, fireEvent, render, screen, waitFor,
} from '@testing-library/react'; } from '@testing-library/react';
import { act } from 'react-dom/test-utils'; import { act } from 'react-dom/test-utils';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
import configureStore from 'redux-mock-store';
import mockTagular from '../../cohesion/utils';
import { RESET_PAGE } from '../../data/constants'; import { RESET_PAGE } from '../../data/constants';
import ChangePasswordPrompt from '../ChangePasswordPrompt'; import ChangePasswordPrompt from '../ChangePasswordPrompt';
const IntlChangePasswordPrompt = injectIntl(ChangePasswordPrompt);
const mockedNavigator = jest.fn(); const mockedNavigator = jest.fn();
const mockStore = configureStore();
mockTagular();
const eventData = {
pageType: 'test-page',
elementType: 'test-element-type',
webElementText: 'test-element-text',
webElementName: 'test-element-name',
};
jest.mock('react-router-dom', () => ({ jest.mock('react-router-dom', () => ({
...(jest.requireActual('react-router-dom')), ...(jest.requireActual('react-router-dom')),
@@ -21,8 +32,14 @@ jest.mock('react-router-dom', () => ({
describe('ChangePasswordPromptTests', () => { describe('ChangePasswordPromptTests', () => {
let props = {}; let props = {};
let store = {};
const initialState = {
cohesion: { eventData: {} },
};
beforeAll(() => { beforeAll(() => {
store = mockStore(initialState);
Object.defineProperty(window, 'matchMedia', { Object.defineProperty(window, 'matchMedia', {
writable: true, writable: true,
value: jest.fn().mockImplementation(query => ({ value: jest.fn().mockImplementation(query => ({
@@ -31,38 +48,56 @@ describe('ChangePasswordPromptTests', () => {
}); });
}); });
it('[nudge modal] should redirect to next url when user clicks close button', () => { it('[nudge modal] should redirect to next url when user clicks close button', async () => {
const dashboardUrl = getConfig().BASE_URL.concat('/dashboard'); const dashboardUrl = getConfig().BASE_URL.concat('/dashboard');
props = { props = {
variant: 'nudge', variant: 'nudge',
redirectUrl: dashboardUrl, redirectUrl: dashboardUrl,
}; };
store = mockStore({
...initialState,
cohesion: {
eventData,
},
});
delete window.location; delete window.location;
window.location = { href: getConfig().BASE_URL }; window.location = { href: getConfig().BASE_URL };
render( render(
<IntlProvider locale="en"> <IntlProvider locale="en">
<MemoryRouter> <Provider store={store}>
<IntlChangePasswordPrompt {...props} /> <MemoryRouter>
</MemoryRouter> <ChangePasswordPrompt {...props} />
</MemoryRouter>
</Provider>
</IntlProvider>, </IntlProvider>,
); );
fireEvent.click(screen.getByText('Close')); fireEvent.click(screen.getByText('Close'));
expect(window.location.href).toBe(dashboardUrl); await waitFor(() => {
expect(window.location.href).toBe(dashboardUrl);
}, { timeout: 1100 });
}); });
it('[block modal] should redirect to reset password page when user clicks outside modal', async () => { it('[block modal] should redirect to reset password page when user clicks outside modal', async () => {
props = { props = {
variant: 'block', variant: 'block',
}; };
store = mockStore({
...initialState,
cohesion: {
eventData,
},
});
render( render(
<IntlProvider locale="en"> <IntlProvider locale="en">
<MemoryRouter> <Provider store={store}>
<IntlChangePasswordPrompt {...props} /> <MemoryRouter>
</MemoryRouter> <ChangePasswordPrompt {...props} />
</MemoryRouter>
</Provider>
</IntlProvider>, </IntlProvider>,
); );

View File

@@ -1,10 +1,12 @@
import React from 'react'; import React from 'react';
import { Provider } from 'react-redux';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n'; import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import { import {
render, screen, render, screen,
} from '@testing-library/react'; } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
import configureStore from 'redux-mock-store';
import { import {
ACCOUNT_LOCKED_OUT, ACCOUNT_LOCKED_OUT,
@@ -25,13 +27,27 @@ import LoginFailureMessage from '../LoginFailure';
jest.mock('@edx/frontend-platform/auth', () => ({ jest.mock('@edx/frontend-platform/auth', () => ({
getAuthService: jest.fn(), getAuthService: jest.fn(),
})); }));
const mockStore = configureStore();
const IntlLoginFailureMessage = injectIntl(LoginFailureMessage); const IntlLoginFailureMessage = injectIntl(LoginFailureMessage);
const eventData = {
pageType: 'test-page',
elementType: 'test-element-type',
webElementText: 'test-element-text',
webElementName: 'test-element-name',
};
describe('LoginFailureMessage', () => { describe('LoginFailureMessage', () => {
let props = {}; let props = {};
let store = {};
const initialState = {
cohesion: { eventData: {} },
};
beforeAll(() => { beforeAll(() => {
store = mockStore(initialState);
Object.defineProperty(window, 'matchMedia', { Object.defineProperty(window, 'matchMedia', {
writable: true, writable: true,
value: jest.fn().mockImplementation(query => ({ value: jest.fn().mockImplementation(query => ({
@@ -48,7 +64,7 @@ describe('LoginFailureMessage', () => {
render( render(
<IntlProvider locale="en"> <IntlProvider locale="en">
<IntlLoginFailureMessage {...props} /> <LoginFailureMessage {...props} />
</IntlProvider>, </IntlProvider>,
); );
@@ -76,7 +92,7 @@ describe('LoginFailureMessage', () => {
render( render(
<IntlProvider locale="en"> <IntlProvider locale="en">
<IntlLoginFailureMessage {...props} /> <LoginFailureMessage {...props} />
</IntlProvider>, </IntlProvider>,
); );
@@ -106,7 +122,7 @@ describe('LoginFailureMessage', () => {
render( render(
<IntlProvider locale="en"> <IntlProvider locale="en">
<IntlLoginFailureMessage {...props} /> <LoginFailureMessage {...props} />
</IntlProvider>, </IntlProvider>,
); );
@@ -132,7 +148,7 @@ describe('LoginFailureMessage', () => {
render( render(
<IntlProvider locale="en"> <IntlProvider locale="en">
<IntlLoginFailureMessage {...props} /> <LoginFailureMessage {...props} />
</IntlProvider>, </IntlProvider>,
); );
@@ -152,7 +168,7 @@ describe('LoginFailureMessage', () => {
render( render(
<IntlProvider locale="en"> <IntlProvider locale="en">
<IntlLoginFailureMessage {...props} /> <LoginFailureMessage {...props} />
</IntlProvider>, </IntlProvider>,
); );
@@ -176,7 +192,7 @@ describe('LoginFailureMessage', () => {
render( render(
<IntlProvider locale="en"> <IntlProvider locale="en">
<IntlLoginFailureMessage {...props} /> <LoginFailureMessage {...props} />
</IntlProvider>, </IntlProvider>,
); );
@@ -196,7 +212,7 @@ describe('LoginFailureMessage', () => {
render( render(
<IntlProvider locale="en"> <IntlProvider locale="en">
<IntlLoginFailureMessage {...props} /> <LoginFailureMessage {...props} />
</IntlProvider>, </IntlProvider>,
); );
@@ -216,7 +232,7 @@ describe('LoginFailureMessage', () => {
render( render(
<IntlProvider locale="en"> <IntlProvider locale="en">
<IntlLoginFailureMessage {...props} /> <LoginFailureMessage {...props} />
</IntlProvider>, </IntlProvider>,
); );
@@ -236,7 +252,7 @@ describe('LoginFailureMessage', () => {
render( render(
<IntlProvider locale="en"> <IntlProvider locale="en">
<IntlLoginFailureMessage {...props} /> <LoginFailureMessage {...props} />
</IntlProvider>, </IntlProvider>,
); );
@@ -255,7 +271,7 @@ describe('LoginFailureMessage', () => {
render( render(
<IntlProvider locale="en"> <IntlProvider locale="en">
<IntlLoginFailureMessage {...props} /> <LoginFailureMessage {...props} />
</IntlProvider>, </IntlProvider>,
); );
@@ -275,7 +291,7 @@ describe('LoginFailureMessage', () => {
render( render(
<IntlProvider locale="en"> <IntlProvider locale="en">
<IntlLoginFailureMessage {...props} /> <LoginFailureMessage {...props} />
</IntlProvider>, </IntlProvider>,
); );
@@ -298,11 +314,19 @@ describe('LoginFailureMessage', () => {
errorCount: 0, errorCount: 0,
}; };
store = mockStore({
...initialState,
cohesion: {
eventData,
},
});
render( render(
<IntlProvider locale="en"> <IntlProvider locale="en">
<MemoryRouter> <Provider store={store}>
<IntlLoginFailureMessage {...props} /> <MemoryRouter>
</MemoryRouter> <IntlLoginFailureMessage {...props} />
</MemoryRouter>
</Provider>
</IntlProvider>, </IntlProvider>,
); );
@@ -323,12 +347,20 @@ describe('LoginFailureMessage', () => {
errorCode: REQUIRE_PASSWORD_CHANGE, errorCode: REQUIRE_PASSWORD_CHANGE,
errorCount: 0, errorCount: 0,
}; };
store = mockStore({
...initialState,
cohesion: {
eventData,
},
});
render( render(
<IntlProvider locale="en"> <IntlProvider locale="en">
<MemoryRouter> <Provider store={store}>
<IntlLoginFailureMessage {...props} /> <MemoryRouter>
</MemoryRouter> <IntlLoginFailureMessage {...props} />
</MemoryRouter>
</Provider>
</IntlProvider>, </IntlProvider>,
); );
@@ -359,7 +391,7 @@ describe('LoginFailureMessage', () => {
render( render(
<IntlProvider locale="en"> <IntlProvider locale="en">
<IntlLoginFailureMessage {...props} /> <LoginFailureMessage {...props} />
</IntlProvider>, </IntlProvider>,
); );

View File

@@ -1,9 +1,8 @@
import React from 'react';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { getConfig, mergeConfig } from '@edx/frontend-platform'; import { getConfig, mergeConfig } from '@edx/frontend-platform';
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics'; import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n'; import { IntlProvider } from '@edx/frontend-platform/i18n';
import { import {
fireEvent, render, screen, waitFor, fireEvent, render, screen, waitFor,
} from '@testing-library/react'; } from '@testing-library/react';
@@ -11,7 +10,10 @@ import { act } from 'react-dom/test-utils';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
import configureStore from 'redux-mock-store'; import configureStore from 'redux-mock-store';
import { COMPLETE_STATE, LOGIN_PAGE, PENDING_STATE } from '../../data/constants'; import mockTagular from '../../cohesion/utils';
import {
APP_NAME, COMPLETE_STATE, LOGIN_PAGE, PENDING_STATE,
} from '../../data/constants';
import { backupLoginFormBegin, dismissPasswordResetBanner, loginRequest } from '../data/actions'; import { backupLoginFormBegin, dismissPasswordResetBanner, loginRequest } from '../data/actions';
import { INTERNAL_SERVER_ERROR } from '../data/constants'; import { INTERNAL_SERVER_ERROR } from '../data/constants';
import LoginPage from '../LoginPage'; import LoginPage from '../LoginPage';
@@ -23,8 +25,8 @@ jest.mock('@edx/frontend-platform/analytics', () => ({
jest.mock('@edx/frontend-platform/auth', () => ({ jest.mock('@edx/frontend-platform/auth', () => ({
getAuthService: jest.fn(), getAuthService: jest.fn(),
})); }));
mockTagular();
const IntlLoginPage = injectIntl(LoginPage);
const mockStore = configureStore(); const mockStore = configureStore();
describe('LoginPage', () => { describe('LoginPage', () => {
@@ -56,6 +58,7 @@ describe('LoginPage', () => {
register: { register: {
validationApiRateLimited: false, validationApiRateLimited: false,
}, },
cohesion: { eventData: {} },
}; };
const secondaryProviders = { const secondaryProviders = {
@@ -88,7 +91,7 @@ describe('LoginPage', () => {
it('should submit form for valid input', () => { it('should submit form for valid input', () => {
store.dispatch = jest.fn(store.dispatch); store.dispatch = jest.fn(store.dispatch);
render(reduxWrapper(<IntlLoginPage {...props} />)); render(reduxWrapper(<LoginPage {...props} />));
fireEvent.change(screen.getByText( fireEvent.change(screen.getByText(
'', '',
@@ -109,7 +112,7 @@ describe('LoginPage', () => {
it('should not dispatch loginRequest on empty form submission', () => { it('should not dispatch loginRequest on empty form submission', () => {
store.dispatch = jest.fn(store.dispatch); store.dispatch = jest.fn(store.dispatch);
render(reduxWrapper(<IntlLoginPage {...props} />)); render(reduxWrapper(<LoginPage {...props} />));
fireEvent.click(screen.getByText( fireEvent.click(screen.getByText(
'', '',
@@ -128,7 +131,7 @@ describe('LoginPage', () => {
}); });
store.dispatch = jest.fn(store.dispatch); store.dispatch = jest.fn(store.dispatch);
render(reduxWrapper(<IntlLoginPage {...props} />)); render(reduxWrapper(<LoginPage {...props} />));
fireEvent.click(screen.getByText( fireEvent.click(screen.getByText(
'', '',
{ selector: '.btn-brand' }, { selector: '.btn-brand' },
@@ -142,7 +145,7 @@ describe('LoginPage', () => {
it('should match state for invalid email (less than 2 characters), on form submission', () => { it('should match state for invalid email (less than 2 characters), on form submission', () => {
store.dispatch = jest.fn(store.dispatch); store.dispatch = jest.fn(store.dispatch);
render(reduxWrapper(<IntlLoginPage {...props} />)); render(reduxWrapper(<LoginPage {...props} />));
fireEvent.change(screen.getByText( fireEvent.change(screen.getByText(
'', '',
@@ -162,7 +165,7 @@ describe('LoginPage', () => {
}); });
it('should show error messages for required fields on empty form submission', () => { it('should show error messages for required fields on empty form submission', () => {
const { container } = render(reduxWrapper(<IntlLoginPage {...props} />)); const { container } = render(reduxWrapper(<LoginPage {...props} />));
fireEvent.click(screen.getByText( fireEvent.click(screen.getByText(
'', '',
{ selector: '.btn-brand' }, { selector: '.btn-brand' },
@@ -176,7 +179,7 @@ describe('LoginPage', () => {
}); });
it('should run frontend validations for emailOrUsername field on form submission', () => { it('should run frontend validations for emailOrUsername field on form submission', () => {
const { container } = render(reduxWrapper(<IntlLoginPage {...props} />)); const { container } = render(reduxWrapper(<LoginPage {...props} />));
fireEvent.change(screen.getByText( fireEvent.change(screen.getByText(
'', '',
@@ -195,7 +198,7 @@ describe('LoginPage', () => {
it('should reset field related error messages on onFocus event', async () => { it('should reset field related error messages on onFocus event', async () => {
store.dispatch = jest.fn(store.dispatch); store.dispatch = jest.fn(store.dispatch);
render(reduxWrapper(<IntlLoginPage {...props} />)); render(reduxWrapper(<LoginPage {...props} />));
await act(async () => { await act(async () => {
// clicking submit button with empty fields to make the errors appear // clicking submit button with empty fields to make the errors appear
@@ -224,7 +227,7 @@ describe('LoginPage', () => {
// ******** test form buttons and links ******** // ******** test form buttons and links ********
it('should match default button state', () => { it('should match default button state', () => {
render(reduxWrapper(<IntlLoginPage {...props} />)); render(reduxWrapper(<LoginPage {...props} />));
expect(screen.getByText('Sign in')).toBeDefined(); expect(screen.getByText('Sign in')).toBeDefined();
}); });
@@ -237,7 +240,7 @@ describe('LoginPage', () => {
}, },
}); });
render(reduxWrapper(<IntlLoginPage {...props} />)); render(reduxWrapper(<LoginPage {...props} />));
expect(screen.getByText( expect(screen.getByText(
'pending', 'pending',
@@ -245,7 +248,7 @@ describe('LoginPage', () => {
}); });
it('should show forgot password link', () => { it('should show forgot password link', () => {
render(reduxWrapper(<IntlLoginPage {...props} />)); render(reduxWrapper(<LoginPage {...props} />));
expect(screen.getByText( expect(screen.getByText(
'Forgot password', 'Forgot password',
@@ -265,7 +268,7 @@ describe('LoginPage', () => {
}, },
}); });
render(reduxWrapper(<IntlLoginPage {...props} />)); render(reduxWrapper(<LoginPage {...props} />));
expect(screen.getByText( expect(screen.getByText(
'', '',
{ selector: `#${ssoProvider.id}` }, { selector: `#${ssoProvider.id}` },
@@ -287,7 +290,7 @@ describe('LoginPage', () => {
}, },
}); });
const { queryByText } = render(reduxWrapper(<IntlLoginPage {...props} />)); const { queryByText } = render(reduxWrapper(<LoginPage {...props} />));
expect(queryByText('Company or school credentials')).toBeNull(); expect(queryByText('Company or school credentials')).toBeNull();
expect(queryByText('Or sign in with:')).toBeNull(); expect(queryByText('Or sign in with:')).toBeNull();
expect(queryByText('Institution/campus credentials')).toBeNull(); expect(queryByText('Institution/campus credentials')).toBeNull();
@@ -307,7 +310,7 @@ describe('LoginPage', () => {
}, },
}); });
const { queryByText } = render(reduxWrapper(<IntlLoginPage {...props} />)); const { queryByText } = render(reduxWrapper(<LoginPage {...props} />));
expect(queryByText('Company or school credentials')).toBeNull(); expect(queryByText('Company or school credentials')).toBeNull();
expect(queryByText('Or sign in with:')).toBeNull(); expect(queryByText('Or sign in with:')).toBeNull();
}); });
@@ -327,7 +330,7 @@ describe('LoginPage', () => {
}, },
}); });
const { queryByText } = render(reduxWrapper(<IntlLoginPage {...props} />)); const { queryByText } = render(reduxWrapper(<LoginPage {...props} />));
expect(queryByText('Or sign in with:')).toBeDefined(); expect(queryByText('Or sign in with:')).toBeDefined();
expect(queryByText('Company or school credentials')).toBeDefined(); expect(queryByText('Company or school credentials')).toBeDefined();
expect(queryByText('Institution/campus credentials')).toBeDefined(); expect(queryByText('Institution/campus credentials')).toBeDefined();
@@ -352,7 +355,7 @@ describe('LoginPage', () => {
}, },
}); });
const { queryByText } = render(reduxWrapper(<IntlLoginPage {...props} />)); const { queryByText } = render(reduxWrapper(<LoginPage {...props} />));
expect(queryByText('Or sign in with:')).toBeDefined(); expect(queryByText('Or sign in with:')).toBeDefined();
expect(queryByText('Company or school credentials')).toBeNull(); expect(queryByText('Company or school credentials')).toBeNull();
expect(queryByText('Institution/campus credentials')).toBeDefined(); expect(queryByText('Institution/campus credentials')).toBeDefined();
@@ -380,7 +383,7 @@ describe('LoginPage', () => {
}, },
}); });
const { queryByText } = render(reduxWrapper(<IntlLoginPage {...props} />)); const { queryByText } = render(reduxWrapper(<LoginPage {...props} />));
expect(queryByText('Or sign in with:')).toBeDefined(); expect(queryByText('Or sign in with:')).toBeDefined();
expect(queryByText('Institution/campus credentials')).toBeDefined(); expect(queryByText('Institution/campus credentials')).toBeDefined();
@@ -400,7 +403,7 @@ describe('LoginPage', () => {
}, },
}); });
const { queryByText } = render(reduxWrapper(<IntlLoginPage {...props} />)); const { queryByText } = render(reduxWrapper(<LoginPage {...props} />));
expect(queryByText('Or sign in with:')).toBeNull(); expect(queryByText('Or sign in with:')).toBeNull();
expect(queryByText('Institution/campus credentials')).toBeNull(); expect(queryByText('Institution/campus credentials')).toBeNull();
expect(queryByText('Company or school credentials')).toBeNull(); expect(queryByText('Company or school credentials')).toBeNull();
@@ -418,7 +421,7 @@ describe('LoginPage', () => {
}, },
}); });
const { queryByText } = render(reduxWrapper(<IntlLoginPage {...props} />)); const { queryByText } = render(reduxWrapper(<LoginPage {...props} />));
expect(queryByText('Or sign in with:')).toBeDefined(); expect(queryByText('Or sign in with:')).toBeDefined();
expect(queryByText('Company or school credentials')).toBeNull(); expect(queryByText('Company or school credentials')).toBeNull();
expect(queryByText('Institution/campus credentials')).toBeDefined(); expect(queryByText('Institution/campus credentials')).toBeDefined();
@@ -441,7 +444,7 @@ describe('LoginPage', () => {
}, },
}); });
render(reduxWrapper(<IntlLoginPage {...props} />)); render(reduxWrapper(<LoginPage {...props} />));
expect(screen.getByText( expect(screen.getByText(
'', '',
{ selector: '#login-failure-alert' }, { selector: '#login-failure-alert' },
@@ -465,7 +468,7 @@ describe('LoginPage', () => {
+ 'linked '}${ getConfig().SITE_NAME } account. To link your accounts, sign in now using your ${ + 'linked '}${ getConfig().SITE_NAME } account. To link your accounts, sign in now using your ${
getConfig().SITE_NAME } password.`; getConfig().SITE_NAME } password.`;
render(reduxWrapper(<IntlLoginPage {...props} />)); render(reduxWrapper(<LoginPage {...props} />));
expect(screen.getByText( expect(screen.getByText(
'', '',
{ selector: '#tpa-alert' }, { selector: '#tpa-alert' },
@@ -484,7 +487,7 @@ describe('LoginPage', () => {
}, },
}, },
}); });
render(reduxWrapper(<IntlLoginPage {...props} />)); render(reduxWrapper(<LoginPage {...props} />));
expect(screen.getByText( expect(screen.getByText(
'', '',
{ selector: '#login-failure-alert' }, { selector: '#login-failure-alert' },
@@ -501,7 +504,7 @@ describe('LoginPage', () => {
}, },
}); });
render(reduxWrapper(<IntlLoginPage {...props} />)); render(reduxWrapper(<LoginPage {...props} />));
expect(screen.getByText( expect(screen.getByText(
'', '',
{ selector: '#login-failure-alert' }, { selector: '#login-failure-alert' },
@@ -510,7 +513,7 @@ describe('LoginPage', () => {
// ******** test redirection ******** // ******** test redirection ********
it('should redirect to url returned by login endpoint after successful authentication', () => { it('should redirect to url returned by login endpoint after successful authentication', async () => {
const dashboardURL = 'https://test.com/testing-dashboard/'; const dashboardURL = 'https://test.com/testing-dashboard/';
store = mockStore({ store = mockStore({
...initialState, ...initialState,
@@ -525,11 +528,13 @@ describe('LoginPage', () => {
delete window.location; delete window.location;
window.location = { href: getConfig().BASE_URL }; window.location = { href: getConfig().BASE_URL };
render(reduxWrapper(<IntlLoginPage {...props} />)); render(reduxWrapper(<LoginPage {...props} />));
expect(window.location.href).toBe(dashboardURL); await waitFor(() => {
expect(window.location.href).toBe(dashboardURL);
}, { timeout: 1100 });
}); });
it('should redirect to finishAuthUrl upon successful login via SSO', () => { it('should redirect to finishAuthUrl upon successful login via SSO', async () => {
const authCompleteUrl = '/auth/complete/google-oauth2/'; const authCompleteUrl = '/auth/complete/google-oauth2/';
store = mockStore({ store = mockStore({
...initialState, ...initialState,
@@ -552,11 +557,13 @@ describe('LoginPage', () => {
delete window.location; delete window.location;
window.location = { href: getConfig().BASE_URL }; window.location = { href: getConfig().BASE_URL };
render(reduxWrapper(<IntlLoginPage {...props} />)); render(reduxWrapper(<LoginPage {...props} />));
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + authCompleteUrl); await waitFor(() => {
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + authCompleteUrl);
}, { timeout: 1100 });
}); });
it('should redirect to social auth provider url on SSO button click', () => { it('should redirect to social auth provider url on SSO button click', async () => {
store = mockStore({ store = mockStore({
...initialState, ...initialState,
commonComponents: { commonComponents: {
@@ -571,16 +578,18 @@ describe('LoginPage', () => {
delete window.location; delete window.location;
window.location = { href: getConfig().BASE_URL }; window.location = { href: getConfig().BASE_URL };
render(reduxWrapper(<IntlLoginPage {...props} />)); render(reduxWrapper(<LoginPage {...props} />));
fireEvent.click(screen.getByText( fireEvent.click(screen.getByText(
'', '',
{ selector: '#oa2-apple-id' }, { selector: '#oa2-apple-id' },
)); ));
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + ssoProvider.loginUrl); await waitFor(() => {
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + ssoProvider.loginUrl);
}, { timeout: 1100 });
}); });
it('should redirect to finishAuthUrl upon successful authentication via SSO', () => { it('should redirect to finishAuthUrl upon successful authentication via SSO', async () => {
const finishAuthUrl = '/auth/complete/google-oauth2/'; const finishAuthUrl = '/auth/complete/google-oauth2/';
store = mockStore({ store = mockStore({
...initialState, ...initialState,
@@ -600,8 +609,10 @@ describe('LoginPage', () => {
delete window.location; delete window.location;
window.location = { href: getConfig().BASE_URL }; window.location = { href: getConfig().BASE_URL };
render(reduxWrapper(<IntlLoginPage {...props} />)); render(reduxWrapper(<LoginPage {...props} />));
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + finishAuthUrl); await waitFor(() => {
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + finishAuthUrl);
}, { timeout: 1100 });
}); });
// ******** test hinted third party auth ******** // ******** test hinted third party auth ********
@@ -622,7 +633,7 @@ describe('LoginPage', () => {
delete window.location; delete window.location;
window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: `?next=/dashboard&tpa_hint=${ssoProvider.id}` }; window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: `?next=/dashboard&tpa_hint=${ssoProvider.id}` };
render(reduxWrapper(<IntlLoginPage {...props} />)); render(reduxWrapper(<LoginPage {...props} />));
expect(screen.getByText( expect(screen.getByText(
'', '',
{ selector: `#${ssoProvider.id}` }, { selector: `#${ssoProvider.id}` },
@@ -649,7 +660,7 @@ describe('LoginPage', () => {
delete window.location; delete window.location;
window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: `?next=/dashboard&tpa_hint=${ssoProvider.id}` }; window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: `?next=/dashboard&tpa_hint=${ssoProvider.id}` };
const { container } = render(reduxWrapper(<IntlLoginPage {...props} />)); const { container } = render(reduxWrapper(<LoginPage {...props} />));
expect(container.querySelector('.react-loading-skeleton')).toBeTruthy(); expect(container.querySelector('.react-loading-skeleton')).toBeTruthy();
}); });
@@ -671,7 +682,7 @@ describe('LoginPage', () => {
window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: `?next=/dashboard&tpa_hint=${secondaryProviders.id}` }; window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: `?next=/dashboard&tpa_hint=${secondaryProviders.id}` };
secondaryProviders.iconImage = null; secondaryProviders.iconImage = null;
render(reduxWrapper(<IntlLoginPage {...props} />)); render(reduxWrapper(<LoginPage {...props} />));
expect(window.location.href).toEqual(getConfig().LMS_BASE_URL + secondaryProviders.loginUrl); expect(window.location.href).toEqual(getConfig().LMS_BASE_URL + secondaryProviders.loginUrl);
}); });
@@ -691,7 +702,7 @@ describe('LoginPage', () => {
delete window.location; delete window.location;
window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: '?next=/dashboard&tpa_hint=invalid' }; window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: '?next=/dashboard&tpa_hint=invalid' };
const { container } = render(reduxWrapper(<IntlLoginPage {...props} />)); const { container } = render(reduxWrapper(<LoginPage {...props} />));
expect(container.querySelector(`#${ssoProvider.id}`).querySelector('#provider-name').textContent).toEqual(`${ssoProvider.name}`); expect(container.querySelector(`#${ssoProvider.id}`).querySelector('#provider-name').textContent).toEqual(`${ssoProvider.name}`);
mergeConfig({ mergeConfig({
@@ -715,7 +726,7 @@ describe('LoginPage', () => {
delete window.location; delete window.location;
window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: `?tpa_hint=${ssoProvider.id}` }; window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: `?tpa_hint=${ssoProvider.id}` };
render(reduxWrapper(<IntlLoginPage {...props} />)); render(reduxWrapper(<LoginPage {...props} />));
expect(screen.getByText( expect(screen.getByText(
'Show me other ways to sign in or register', 'Show me other ways to sign in or register',
).textContent).toBeDefined(); ).textContent).toBeDefined();
@@ -741,7 +752,7 @@ describe('LoginPage', () => {
delete window.location; delete window.location;
window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: `?tpa_hint=${ssoProvider.id}` }; window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: `?tpa_hint=${ssoProvider.id}` };
render(reduxWrapper(<IntlLoginPage {...props} />)); render(reduxWrapper(<LoginPage {...props} />));
expect(screen.getByText( expect(screen.getByText(
'Show me other ways to sign in', 'Show me other ways to sign in',
).textContent).toBeDefined(); ).textContent).toBeDefined();
@@ -750,8 +761,8 @@ describe('LoginPage', () => {
// ******** miscellaneous tests ******** // ******** miscellaneous tests ********
it('should send page event when login page is rendered', () => { it('should send page event when login page is rendered', () => {
render(reduxWrapper(<IntlLoginPage {...props} />)); render(reduxWrapper(<LoginPage {...props} />));
expect(sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'login'); expect(sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'login', { app_name: APP_NAME });
}); });
it('tests that form is in invalid state when it is submitted', () => { it('tests that form is in invalid state when it is submitted', () => {
@@ -764,7 +775,7 @@ describe('LoginPage', () => {
}); });
store.dispatch = jest.fn(store.dispatch); store.dispatch = jest.fn(store.dispatch);
render(reduxWrapper(<IntlLoginPage {...props} />)); render(reduxWrapper(<LoginPage {...props} />));
expect(store.dispatch).toHaveBeenCalledWith(backupLoginFormBegin( expect(store.dispatch).toHaveBeenCalledWith(backupLoginFormBegin(
{ {
formFields: { formFields: {
@@ -778,13 +789,13 @@ describe('LoginPage', () => {
}); });
it('should send track event when forgot password link is clicked', () => { it('should send track event when forgot password link is clicked', () => {
render(reduxWrapper(<IntlLoginPage {...props} />)); render(reduxWrapper(<LoginPage {...props} />));
fireEvent.click(screen.getByText( fireEvent.click(screen.getByText(
'Forgot password', 'Forgot password',
{ selector: '#forgot-password' }, { selector: '#forgot-password' },
)); ));
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.password-reset_form.toggled', { category: 'user-engagement' }); expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.password-reset_form.toggled', { category: 'user-engagement', app_name: APP_NAME });
}); });
it('should backup the login form state when shouldBackupState is true', () => { it('should backup the login form state when shouldBackupState is true', () => {
@@ -797,7 +808,7 @@ describe('LoginPage', () => {
}); });
store.dispatch = jest.fn(store.dispatch); store.dispatch = jest.fn(store.dispatch);
render(reduxWrapper(<IntlLoginPage {...props} />)); render(reduxWrapper(<LoginPage {...props} />));
expect(store.dispatch).toHaveBeenCalledWith(backupLoginFormBegin( expect(store.dispatch).toHaveBeenCalledWith(backupLoginFormBegin(
{ {
formFields: { formFields: {
@@ -826,7 +837,7 @@ describe('LoginPage', () => {
}, },
}); });
const { container } = render(reduxWrapper(<IntlLoginPage {...props} />)); const { container } = render(reduxWrapper(<LoginPage {...props} />));
expect(container.querySelector('input#emailOrUsername').value).toEqual('john_doe'); expect(container.querySelector('input#emailOrUsername').value).toEqual('john_doe');
expect(container.querySelector('input#password').value).toEqual('test-password'); expect(container.querySelector('input#password').value).toEqual('test-password');
}); });

View File

@@ -20,7 +20,7 @@ import {
tpaProvidersSelector, tpaProvidersSelector,
} from '../common-components/data/selectors'; } from '../common-components/data/selectors';
import messages from '../common-components/messages'; import messages from '../common-components/messages';
import { LOGIN_PAGE, REGISTER_PAGE } from '../data/constants'; import { APP_NAME, LOGIN_PAGE, REGISTER_PAGE } from '../data/constants';
import { import {
getTpaHint, getTpaProvider, updatePathWithQueryParams, getTpaHint, getTpaProvider, updatePathWithQueryParams,
} from '../data/utils'; } from '../data/utils';
@@ -56,11 +56,11 @@ const Logistration = (props) => {
}, [navigate, disablePublicAccountCreation]); }, [navigate, disablePublicAccountCreation]);
const handleInstitutionLogin = (e) => { const handleInstitutionLogin = (e) => {
sendTrackEvent('edx.bi.institution_login_form.toggled', { category: 'user-engagement' }); sendTrackEvent('edx.bi.institution_login_form.toggled', { category: 'user-engagement', app_name: APP_NAME });
if (typeof e === 'string') { if (typeof e === 'string') {
sendPageEvent('login_and_registration', e === '/login' ? 'login' : 'register'); sendPageEvent('login_and_registration', e === '/login' ? 'login' : 'register', { app_name: APP_NAME });
} else { } else {
sendPageEvent('login_and_registration', e.target.dataset.eventName); sendPageEvent('login_and_registration', e.target.dataset.eventName, { app_name: APP_NAME });
} }
setInstitutionLogin(!institutionLogin); setInstitutionLogin(!institutionLogin);
@@ -70,7 +70,8 @@ const Logistration = (props) => {
if (tabKey === currentTab) { if (tabKey === currentTab) {
return; return;
} }
sendTrackEvent(`edx.bi.${tabKey.replace('/', '')}_form.toggled`, { category: 'user-engagement' });
sendTrackEvent(`edx.bi.${tabKey.replace('/', '')}_form.toggled`, { category: 'user-engagement', app_name: APP_NAME });
props.clearThirdPartyAuthContextErrorMessage(); props.clearThirdPartyAuthContextErrorMessage();
if (tabKey === LOGIN_PAGE) { if (tabKey === LOGIN_PAGE) {
props.backupRegistrationForm(); props.backupRegistrationForm();

View File

@@ -1,9 +1,8 @@
import React from 'react';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { getConfig, mergeConfig } from '@edx/frontend-platform'; import { getConfig, mergeConfig } from '@edx/frontend-platform';
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics'; import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
import { configure, injectIntl, IntlProvider } from '@edx/frontend-platform/i18n'; import { configure, IntlProvider } from '@edx/frontend-platform/i18n';
import { fireEvent, render, screen } from '@testing-library/react'; import { fireEvent, render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
import configureStore from 'redux-mock-store'; import configureStore from 'redux-mock-store';
@@ -11,19 +10,23 @@ import configureStore from 'redux-mock-store';
import Logistration from './Logistration'; import Logistration from './Logistration';
import { clearThirdPartyAuthContextErrorMessage } from '../common-components/data/actions'; import { clearThirdPartyAuthContextErrorMessage } from '../common-components/data/actions';
import { import {
APP_NAME,
COMPLETE_STATE, LOGIN_PAGE, REGISTER_PAGE, COMPLETE_STATE, LOGIN_PAGE, REGISTER_PAGE,
} from '../data/constants'; } from '../data/constants';
import { backupLoginForm } from '../login/data/actions'; import { backupLoginForm } from '../login/data/actions';
import { backupRegistrationForm } from '../register/data/actions'; import { backupRegistrationForm } from '../register/data/actions';
import { NOT_INITIALIZED } from '../register/data/optimizelyExperiment/helper';
import useAutoGeneratedUsernameExperimentVariation
from '../register/data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation';
jest.mock('@edx/frontend-platform/analytics', () => ({ jest.mock('@edx/frontend-platform/analytics', () => ({
sendPageEvent: jest.fn(), sendPageEvent: jest.fn(),
sendTrackEvent: jest.fn(), sendTrackEvent: jest.fn(),
})); }));
jest.mock('@edx/frontend-platform/auth'); jest.mock('@edx/frontend-platform/auth');
jest.mock('../register/data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation', () => jest.fn());
const mockStore = configureStore(); const mockStore = configureStore();
const IntlLogistration = injectIntl(Logistration);
describe('Logistration', () => { describe('Logistration', () => {
let store = {}; let store = {};
@@ -64,6 +67,7 @@ describe('Logistration', () => {
usernameSuggestions: [], usernameSuggestions: [],
validationApiRateLimited: false, validationApiRateLimited: false,
}, },
cohesion: { eventData: {} },
commonComponents: { commonComponents: {
thirdPartyAuthContext: { thirdPartyAuthContext: {
providers: [], providers: [],
@@ -84,6 +88,7 @@ describe('Logistration', () => {
})), })),
})); }));
useAutoGeneratedUsernameExperimentVariation.mockReturnValue(NOT_INITIALIZED);
configure({ configure({
loggingService: { logError: jest.fn() }, loggingService: { logError: jest.fn() },
config: { config: {
@@ -95,7 +100,7 @@ describe('Logistration', () => {
}); });
it('should do nothing when user clicks on the same tab (login/register) again', () => { it('should do nothing when user clicks on the same tab (login/register) again', () => {
const { container } = render(reduxWrapper(<IntlLogistration />)); const { container } = render(reduxWrapper(<Logistration />));
// While staying on the registration form, clicking the register tab again // While staying on the registration form, clicking the register tab again
fireEvent.click(container.querySelector('a[data-rb-event-key="/register"]')); fireEvent.click(container.querySelector('a[data-rb-event-key="/register"]'));
@@ -107,14 +112,14 @@ describe('Logistration', () => {
ALLOW_PUBLIC_ACCOUNT_CREATION: true, ALLOW_PUBLIC_ACCOUNT_CREATION: true,
}); });
const { container } = render(reduxWrapper(<IntlLogistration />)); const { container } = render(reduxWrapper(<Logistration />));
expect(container.querySelector('RegistrationPage')).toBeDefined(); expect(container.querySelector('RegistrationPage')).toBeDefined();
}); });
it('should render login page', () => { it('should render login page', () => {
const props = { selectedPage: LOGIN_PAGE }; const props = { selectedPage: LOGIN_PAGE };
const { container } = render(reduxWrapper(<IntlLogistration {...props} />)); const { container } = render(reduxWrapper(<Logistration {...props} />));
expect(container.querySelector('LoginPage')).toBeDefined(); expect(container.querySelector('LoginPage')).toBeDefined();
}); });
@@ -125,7 +130,7 @@ describe('Logistration', () => {
}); });
let props = { selectedPage: LOGIN_PAGE }; let props = { selectedPage: LOGIN_PAGE };
const { rerender } = render(reduxWrapper(<IntlLogistration {...props} />)); const { rerender } = render(reduxWrapper(<Logistration {...props} />));
// verifying sign in heading // verifying sign in heading
expect(screen.getByRole('heading', { level: 3 }).textContent).toEqual('Sign in'); expect(screen.getByRole('heading', { level: 3 }).textContent).toEqual('Sign in');
@@ -133,7 +138,7 @@ describe('Logistration', () => {
// register page is still accessible when SHOW_REGISTRATION_LINKS is false // register page is still accessible when SHOW_REGISTRATION_LINKS is false
// but it needs to be accessed directly // but it needs to be accessed directly
props = { selectedPage: REGISTER_PAGE }; props = { selectedPage: REGISTER_PAGE };
rerender(reduxWrapper(<IntlLogistration {...props} />)); rerender(reduxWrapper(<Logistration {...props} />));
// verifying register heading // verifying register heading
expect(screen.getByRole('heading', { level: 3 }).textContent).toEqual('Register'); expect(screen.getByRole('heading', { level: 3 }).textContent).toEqual('Register');
@@ -160,7 +165,7 @@ describe('Logistration', () => {
}); });
const props = { selectedPage: LOGIN_PAGE }; const props = { selectedPage: LOGIN_PAGE };
const { container } = render(reduxWrapper(<IntlLogistration {...props} />)); const { container } = render(reduxWrapper(<Logistration {...props} />));
// verifying sign in heading for institution login false // verifying sign in heading for institution login false
expect(screen.getByRole('heading', { level: 3 }).textContent).toEqual('Sign in'); expect(screen.getByRole('heading', { level: 3 }).textContent).toEqual('Sign in');
@@ -190,7 +195,7 @@ describe('Logistration', () => {
}); });
const props = { selectedPage: LOGIN_PAGE }; const props = { selectedPage: LOGIN_PAGE };
render(reduxWrapper(<IntlLogistration {...props} />)); render(reduxWrapper(<Logistration {...props} />));
expect(screen.getByText('Institution/campus credentials')).toBeDefined(); expect(screen.getByText('Institution/campus credentials')).toBeDefined();
// on clicking "Institution/campus credentials" button, it should display institution login page // on clicking "Institution/campus credentials" button, it should display institution login page
@@ -221,11 +226,11 @@ describe('Logistration', () => {
}); });
const props = { selectedPage: LOGIN_PAGE }; const props = { selectedPage: LOGIN_PAGE };
render(reduxWrapper(<IntlLogistration {...props} />)); render(reduxWrapper(<Logistration {...props} />));
fireEvent.click(screen.getByText('Institution/campus credentials')); fireEvent.click(screen.getByText('Institution/campus credentials'));
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.institution_login_form.toggled', { category: 'user-engagement' }); expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.institution_login_form.toggled', { category: 'user-engagement', app_name: APP_NAME });
expect(sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'institution_login'); expect(sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'institution_login', { app_name: APP_NAME });
mergeConfig({ mergeConfig({
DISABLE_ENTERPRISE_LOGIN: '', DISABLE_ENTERPRISE_LOGIN: '',
@@ -253,7 +258,7 @@ describe('Logistration', () => {
delete window.location; delete window.location;
window.location = { hostname: getConfig().SITE_NAME, href: getConfig().BASE_URL }; window.location = { hostname: getConfig().SITE_NAME, href: getConfig().BASE_URL };
render(reduxWrapper(<IntlLogistration />)); render(reduxWrapper(<Logistration />));
fireEvent.click(screen.getByText('Institution/campus credentials')); fireEvent.click(screen.getByText('Institution/campus credentials'));
expect(screen.getByText('Test University')).toBeDefined(); expect(screen.getByText('Test University')).toBeDefined();
@@ -264,7 +269,7 @@ describe('Logistration', () => {
it('should fire action to backup registration form on tab click', () => { it('should fire action to backup registration form on tab click', () => {
store.dispatch = jest.fn(store.dispatch); store.dispatch = jest.fn(store.dispatch);
const { container } = render(reduxWrapper(<IntlLogistration />)); const { container } = render(reduxWrapper(<Logistration />));
fireEvent.click(container.querySelector('a[data-rb-event-key="/login"]')); fireEvent.click(container.querySelector('a[data-rb-event-key="/login"]'));
expect(store.dispatch).toHaveBeenCalledWith(backupRegistrationForm()); expect(store.dispatch).toHaveBeenCalledWith(backupRegistrationForm());
}); });
@@ -272,14 +277,14 @@ describe('Logistration', () => {
it('should fire action to backup login form on tab click', () => { it('should fire action to backup login form on tab click', () => {
store.dispatch = jest.fn(store.dispatch); store.dispatch = jest.fn(store.dispatch);
const props = { selectedPage: LOGIN_PAGE }; const props = { selectedPage: LOGIN_PAGE };
const { container } = render(reduxWrapper(<IntlLogistration {...props} />)); const { container } = render(reduxWrapper(<Logistration {...props} />));
fireEvent.click(container.querySelector('a[data-rb-event-key="/register"]')); fireEvent.click(container.querySelector('a[data-rb-event-key="/register"]'));
expect(store.dispatch).toHaveBeenCalledWith(backupLoginForm()); expect(store.dispatch).toHaveBeenCalledWith(backupLoginForm());
}); });
it('should clear tpa context errorMessage tab click', () => { it('should clear tpa context errorMessage tab click', () => {
store.dispatch = jest.fn(store.dispatch); store.dispatch = jest.fn(store.dispatch);
const { container } = render(reduxWrapper(<IntlLogistration />)); const { container } = render(reduxWrapper(<Logistration />));
fireEvent.click(container.querySelector('a[data-rb-event-key="/login"]')); fireEvent.click(container.querySelector('a[data-rb-event-key="/login"]'));
expect(store.dispatch).toHaveBeenCalledWith(clearThirdPartyAuthContextErrorMessage()); expect(store.dispatch).toHaveBeenCalledWith(clearThirdPartyAuthContextErrorMessage());
}); });

View File

@@ -0,0 +1,29 @@
import { PluginSlot } from '@openedx/frontend-plugin-framework';
import { render } from '@testing-library/react';
import MainAppSlot from './index';
jest.mock('@openedx/frontend-plugin-framework', () => ({
PluginSlot: jest.fn(() => null),
}));
describe('MainAppSlot', () => {
it('renders without crashing', () => {
render(<MainAppSlot />);
});
it('renders a PluginSlot component', () => {
render(<MainAppSlot />);
expect(PluginSlot).toHaveBeenCalled();
});
it('passes the correct id prop to PluginSlot', () => {
render(<MainAppSlot />);
expect(PluginSlot).toHaveBeenCalledWith({ id: 'main_app_slot' }, {});
});
it('does not render any children', () => {
const { container } = render(<MainAppSlot />);
expect(container.firstChild).toBeNull();
});
});

View File

@@ -0,0 +1,41 @@
# Main App Slot
### Slot ID: `main_app_slot`
## Description
This slot is used for adding content at the root level.
## Example
The following `env.config.jsx` will render a component at the MFE root level.
![Screenshot of Content added after the Main App Slot](./images/main_app_slot.png)
```js
import {
DIRECT_PLUGIN,
PLUGIN_OPERATIONS,
} from "@openedx/frontend-plugin-framework";
import { ExampleComponent } from "@openedx/frontend-plugin-example";
const config = {
pluginSlots: {
main_app_slot: {
plugins: [
{
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: "example-component",
type: DIRECT_PLUGIN,
priority: 60,
RenderWidget: ExampleComponent,
},
},
],
},
},
};
export default config;
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 KiB

View File

@@ -0,0 +1,7 @@
import { PluginSlot } from '@openedx/frontend-plugin-framework';
const MainAppSlot = () => (
<PluginSlot id="main_app_slot" />
);
export default MainAppSlot;

View File

@@ -0,0 +1,3 @@
# `frontend-app-authn` Plugin Slots
- [`main_app_slot`](./MainAppSlot/)

View File

@@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { getConfig, snakeCaseObject } from '@edx/frontend-platform'; import { getConfig, snakeCaseObject } from '@edx/frontend-platform';
import { identifyAuthenticatedUser, sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics'; import { identifyAuthenticatedUser } from '@edx/frontend-platform/analytics';
import { import {
AxiosJwtAuthService, AxiosJwtAuthService,
configure as configureAuth, configure as configureAuth,
@@ -39,6 +39,13 @@ import {
import isOneTrustFunctionalCookieEnabled from '../data/oneTrust'; import isOneTrustFunctionalCookieEnabled from '../data/oneTrust';
import { getAllPossibleQueryParams, isHostAvailableInQueryParams } from '../data/utils'; import { getAllPossibleQueryParams, isHostAvailableInQueryParams } from '../data/utils';
import { FormFieldRenderer } from '../field-renderer'; import { FormFieldRenderer } from '../field-renderer';
import {
trackDisablePostRegistrationRecommendations,
trackProgressiveProfilingPageViewed,
trackProgressiveProfilingSkipLinkClick,
trackProgressiveProfilingSubmitClick,
trackProgressiveProfilingSupportLinkCLick,
} from '../tracking/trackers/progressive-profiling';
const ProgressiveProfiling = (props) => { const ProgressiveProfiling = (props) => {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
@@ -98,14 +105,13 @@ const ProgressiveProfiling = (props) => {
useEffect(() => { useEffect(() => {
if (authenticatedUser?.userId) { if (authenticatedUser?.userId) {
identifyAuthenticatedUser(authenticatedUser.userId); identifyAuthenticatedUser(authenticatedUser.userId);
sendPageEvent('login_and_registration', 'welcome'); trackProgressiveProfilingPageViewed();
} }
}, [authenticatedUser]); }, [authenticatedUser]);
useEffect(() => { useEffect(() => {
if (!enablePostRegistrationRecommendations) { if (!enablePostRegistrationRecommendations) {
sendTrackEvent( trackDisablePostRegistrationRecommendations(
'edx.bi.user.recommendations.not.enabled',
{ functionalCookiesConsent, page: 'authn_recommendations' }, { functionalCookiesConsent, page: 'authn_recommendations' },
); );
return; return;
@@ -149,29 +155,23 @@ const ProgressiveProfiling = (props) => {
}); });
} }
props.saveUserProfile(authenticatedUser.username, snakeCaseObject(payload)); props.saveUserProfile(authenticatedUser.username, snakeCaseObject(payload));
const eventProperties = {
sendTrackEvent( isGenderSelected: !!values.gender,
'edx.bi.welcome.page.submit.clicked', isYearOfBirthSelected: !!values.year_of_birth,
{ isLevelOfEducationSelected: !!values.level_of_education,
isGenderSelected: !!values.gender, isWorkExperienceSelected: !!values.work_experience,
isYearOfBirthSelected: !!values.year_of_birth, host: queryParams?.host || '',
isLevelOfEducationSelected: !!values.level_of_education, };
isWorkExperienceSelected: !!values.work_experience, trackProgressiveProfilingSubmitClick(eventProperties);
host: queryParams?.host || '',
},
);
}; };
const handleSkip = (e) => { const handleSkip = (e) => {
e.preventDefault(); e.preventDefault();
window.history.replaceState(location.state, null, ''); window.history.replaceState(location.state, null, '');
setShowModal(true); setShowModal(true);
sendTrackEvent( trackProgressiveProfilingSkipLinkClick({
'edx.bi.welcome.page.skip.link.clicked', host: queryParams?.host || '',
{ });
host: queryParams?.host || '',
},
);
}; };
const onChangeHandler = (e) => { const onChangeHandler = (e) => {
@@ -242,7 +242,7 @@ const ProgressiveProfiling = (props) => {
destination={getConfig().AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK} destination={getConfig().AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK}
target="_blank" target="_blank"
showLaunchIcon={false} showLaunchIcon={false}
onClick={() => (sendTrackEvent('edx.bi.welcome.page.support.link.clicked'))} onClick={() => (trackProgressiveProfilingSupportLinkCLick())}
> >
{formatMessage(messages['optional.fields.information.link'])} {formatMessage(messages['optional.fields.information.link'])}
</Hyperlink> </Hyperlink>

View File

@@ -1,17 +1,18 @@
import React from 'react';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { getConfig, mergeConfig } from '@edx/frontend-platform'; import { getConfig, mergeConfig } from '@edx/frontend-platform';
import { identifyAuthenticatedUser, sendTrackEvent } from '@edx/frontend-platform/analytics'; import { identifyAuthenticatedUser, sendTrackEvent } from '@edx/frontend-platform/analytics';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth'; import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { configure, injectIntl, IntlProvider } from '@edx/frontend-platform/i18n'; import { configure, IntlProvider } from '@edx/frontend-platform/i18n';
import { import {
fireEvent, render, screen, fireEvent, render, screen, waitFor,
} from '@testing-library/react'; } from '@testing-library/react';
import { MemoryRouter, mockNavigate, useLocation } from 'react-router-dom'; import { MemoryRouter, mockNavigate, useLocation } from 'react-router-dom';
import configureStore from 'redux-mock-store'; import configureStore from 'redux-mock-store';
import mockTagular from '../../cohesion/utils';
import { import {
APP_NAME,
AUTHN_PROGRESSIVE_PROFILING, AUTHN_PROGRESSIVE_PROFILING,
COMPLETE_STATE, DEFAULT_REDIRECT_URL, COMPLETE_STATE, DEFAULT_REDIRECT_URL,
EMBEDDED, EMBEDDED,
@@ -22,8 +23,8 @@ import {
import { saveUserProfile } from '../data/actions'; import { saveUserProfile } from '../data/actions';
import ProgressiveProfiling from '../ProgressiveProfiling'; import ProgressiveProfiling from '../ProgressiveProfiling';
const IntlProgressiveProfilingPage = injectIntl(ProgressiveProfiling);
const mockStore = configureStore(); const mockStore = configureStore();
mockTagular();
jest.mock('@edx/frontend-platform/analytics', () => ({ jest.mock('@edx/frontend-platform/analytics', () => ({
sendPageEvent: jest.fn(), sendPageEvent: jest.fn(),
@@ -54,6 +55,13 @@ jest.mock('react-router-dom', () => {
}; };
}); });
const eventData = {
pageType: 'test-page',
elementType: 'test-element-type',
webElementText: 'test-element-text',
webElementName: 'test-element-name',
};
describe('ProgressiveProfilingTests', () => { describe('ProgressiveProfilingTests', () => {
let store = {}; let store = {};
@@ -114,7 +122,7 @@ describe('ProgressiveProfilingTests', () => {
mergeConfig({ mergeConfig({
AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK: '', AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK: '',
}); });
const { queryByRole } = render(reduxWrapper(<IntlProgressiveProfilingPage />)); const { queryByRole } = render(reduxWrapper(<ProgressiveProfiling />));
const button = queryByRole('button', { name: /learn more about how we use this information/i }); const button = queryByRole('button', { name: /learn more about how we use this information/i });
expect(button).toBeNull(); expect(button).toBeNull();
@@ -125,7 +133,7 @@ describe('ProgressiveProfilingTests', () => {
AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK: 'http://localhost:1999/support', AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK: 'http://localhost:1999/support',
}); });
const { getByText } = render(reduxWrapper(<IntlProgressiveProfilingPage />)); const { getByText } = render(reduxWrapper(<ProgressiveProfiling />));
const learnMoreButton = getByText('Learn more about how we use this information.'); const learnMoreButton = getByText('Learn more about how we use this information.');
@@ -135,7 +143,7 @@ describe('ProgressiveProfilingTests', () => {
it('should open modal on pressing skip for now button', () => { it('should open modal on pressing skip for now button', () => {
delete window.location; delete window.location;
window.location = { href: getConfig().BASE_URL.concat(AUTHN_PROGRESSIVE_PROFILING) }; window.location = { href: getConfig().BASE_URL.concat(AUTHN_PROGRESSIVE_PROFILING) };
const { getByRole } = render(reduxWrapper(<IntlProgressiveProfilingPage />)); const { getByRole } = render(reduxWrapper(<ProgressiveProfiling />));
const skipButton = getByRole('button', { name: /skip for now/i }); const skipButton = getByRole('button', { name: /skip for now/i });
fireEvent.click(skipButton); fireEvent.click(skipButton);
@@ -143,14 +151,15 @@ describe('ProgressiveProfilingTests', () => {
const modalContentContainer = document.getElementsByClassName('.pgn__modal-content-container'); const modalContentContainer = document.getElementsByClassName('.pgn__modal-content-container');
expect(modalContentContainer).toBeTruthy(); expect(modalContentContainer).toBeTruthy();
const payload = { host: '', app_name: APP_NAME };
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.welcome.page.skip.link.clicked', { host: '' }); expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.welcome.page.skip.link.clicked', payload);
}); });
// ******** test event functionality ******** // ******** test event functionality ********
it('should make identify call to segment on progressive profiling page', () => { it('should make identify call to segment on progressive profiling page', () => {
render(reduxWrapper(<IntlProgressiveProfilingPage />)); render(reduxWrapper(<ProgressiveProfiling />));
expect(identifyAuthenticatedUser).toHaveBeenCalledWith(3); expect(identifyAuthenticatedUser).toHaveBeenCalledWith(3);
expect(identifyAuthenticatedUser).toHaveBeenCalled(); expect(identifyAuthenticatedUser).toHaveBeenCalled();
@@ -160,12 +169,12 @@ describe('ProgressiveProfilingTests', () => {
mergeConfig({ mergeConfig({
AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK: 'http://localhost:1999/support', AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK: 'http://localhost:1999/support',
}); });
render(reduxWrapper(<IntlProgressiveProfilingPage />)); render(reduxWrapper(<ProgressiveProfiling />));
const supportLink = screen.getByRole('link', { name: /learn more about how we use this information/i }); const supportLink = screen.getByRole('link', { name: /learn more about how we use this information/i });
fireEvent.click(supportLink); fireEvent.click(supportLink);
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.welcome.page.support.link.clicked'); expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.welcome.page.support.link.clicked', { app_name: APP_NAME });
}); });
it('should set empty host property value for non-embedded experience', () => { it('should set empty host property value for non-embedded experience', () => {
@@ -175,10 +184,11 @@ describe('ProgressiveProfilingTests', () => {
isLevelOfEducationSelected: false, isLevelOfEducationSelected: false,
isWorkExperienceSelected: false, isWorkExperienceSelected: false,
host: '', host: '',
app_name: APP_NAME,
}; };
delete window.location; delete window.location;
window.location = { href: getConfig().BASE_URL.concat(AUTHN_PROGRESSIVE_PROFILING) }; window.location = { href: getConfig().BASE_URL.concat(AUTHN_PROGRESSIVE_PROFILING) };
render(reduxWrapper(<IntlProgressiveProfilingPage />)); render(reduxWrapper(<ProgressiveProfiling />));
const nextButton = screen.getByText('Next'); const nextButton = screen.getByText('Next');
fireEvent.click(nextButton); fireEvent.click(nextButton);
@@ -194,7 +204,7 @@ describe('ProgressiveProfilingTests', () => {
extended_profile: [{ field_name: 'company', field_value: 'test company' }], extended_profile: [{ field_name: 'company', field_value: 'test company' }],
}; };
store.dispatch = jest.fn(store.dispatch); store.dispatch = jest.fn(store.dispatch);
const { getByLabelText, getByText } = render(reduxWrapper(<IntlProgressiveProfilingPage />)); const { getByLabelText, getByText } = render(reduxWrapper(<ProgressiveProfiling />));
const genderSelect = getByLabelText('Gender'); const genderSelect = getByLabelText('Gender');
const companyInput = getByLabelText('Company'); const companyInput = getByLabelText('Company');
@@ -216,7 +226,7 @@ describe('ProgressiveProfilingTests', () => {
}, },
}); });
const { container } = render(reduxWrapper(<IntlProgressiveProfilingPage />)); const { container } = render(reduxWrapper(<ProgressiveProfiling />));
const errorElement = container.querySelector('#pp-page-errors'); const errorElement = container.querySelector('#pp-page-errors');
expect(errorElement).toBeTruthy(); expect(errorElement).toBeTruthy();
@@ -232,7 +242,7 @@ describe('ProgressiveProfilingTests', () => {
href: getConfig().BASE_URL, href: getConfig().BASE_URL,
}; };
render(reduxWrapper(<IntlProgressiveProfilingPage />)); render(reduxWrapper(<ProgressiveProfiling />));
expect(window.location.href).toEqual(DASHBOARD_URL); expect(window.location.href).toEqual(DASHBOARD_URL);
}); });
@@ -249,8 +259,11 @@ describe('ProgressiveProfilingTests', () => {
...initialState.welcomePage, ...initialState.welcomePage,
success: true, success: true,
}, },
cohesion: {
eventData,
},
}); });
const { container } = render(reduxWrapper(<IntlProgressiveProfilingPage />)); const { container } = render(reduxWrapper(<ProgressiveProfiling />));
const nextButton = container.querySelector('button.btn-brand'); const nextButton = container.querySelector('button.btn-brand');
expect(nextButton.textContent).toEqual('Next'); expect(nextButton.textContent).toEqual('Next');
@@ -275,13 +288,18 @@ describe('ProgressiveProfilingTests', () => {
...initialState.welcomePage, ...initialState.welcomePage,
success: true, success: true,
}, },
cohesion: {
eventData,
},
}); });
const { container } = render(reduxWrapper(<IntlProgressiveProfilingPage />)); const { container } = render(reduxWrapper(<ProgressiveProfiling />));
const nextButton = container.querySelector('button.btn-brand'); const nextButton = container.querySelector('button.btn-brand');
expect(nextButton.textContent).toEqual('Submit'); expect(nextButton.textContent).toEqual('Submit');
expect(window.location.href).toEqual(redirectUrl); await waitFor(() => {
expect(window.location.href).toEqual(redirectUrl);
}, { timeout: 1100 });
}); });
}); });
@@ -311,12 +329,12 @@ describe('ProgressiveProfilingTests', () => {
href: getConfig().BASE_URL.concat(AUTHN_PROGRESSIVE_PROFILING), href: getConfig().BASE_URL.concat(AUTHN_PROGRESSIVE_PROFILING),
search: `?host=${host}&variant=${EMBEDDED}`, search: `?host=${host}&variant=${EMBEDDED}`,
}; };
render(reduxWrapper(<IntlProgressiveProfilingPage />)); render(reduxWrapper(<ProgressiveProfiling />));
const skipLinkButton = screen.getByText('Skip for now'); const skipLinkButton = screen.getByText('Skip for now');
fireEvent.click(skipLinkButton); fireEvent.click(skipLinkButton);
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.welcome.page.skip.link.clicked', { host }); expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.welcome.page.skip.link.clicked', { host, app_name: APP_NAME });
}); });
it('should show spinner while fetching the optional fields', () => { it('should show spinner while fetching the optional fields', () => {
@@ -336,7 +354,7 @@ describe('ProgressiveProfilingTests', () => {
}, },
}); });
const { container } = render(reduxWrapper(<IntlProgressiveProfilingPage />)); const { container } = render(reduxWrapper(<ProgressiveProfiling />));
const tpaSpinnerElement = container.querySelector('#tpa-spinner'); const tpaSpinnerElement = container.querySelector('#tpa-spinner');
expect(tpaSpinnerElement).toBeTruthy(); expect(tpaSpinnerElement).toBeTruthy();
@@ -349,13 +367,14 @@ describe('ProgressiveProfilingTests', () => {
isLevelOfEducationSelected: false, isLevelOfEducationSelected: false,
isWorkExperienceSelected: false, isWorkExperienceSelected: false,
host: 'http://example.com', host: 'http://example.com',
app_name: APP_NAME,
}; };
delete window.location; delete window.location;
window.location = { window.location = {
href: getConfig().BASE_URL.concat(AUTHN_PROGRESSIVE_PROFILING), href: getConfig().BASE_URL.concat(AUTHN_PROGRESSIVE_PROFILING),
search: `?host=${host}`, search: `?host=${host}`,
}; };
render(reduxWrapper(<IntlProgressiveProfilingPage />)); render(reduxWrapper(<ProgressiveProfiling />));
const submitButton = screen.getByText('Next'); const submitButton = screen.getByText('Next');
fireEvent.click(submitButton); fireEvent.click(submitButton);
@@ -370,7 +389,7 @@ describe('ProgressiveProfilingTests', () => {
search: `?variant=${EMBEDDED}&host=${host}`, search: `?variant=${EMBEDDED}&host=${host}`,
}; };
const { container } = render(reduxWrapper(<IntlProgressiveProfilingPage />)); const { container } = render(reduxWrapper(<ProgressiveProfiling />));
const genderField = container.querySelector('#gender'); const genderField = container.querySelector('#gender');
expect(genderField).toBeTruthy(); expect(genderField).toBeTruthy();
@@ -391,11 +410,11 @@ describe('ProgressiveProfilingTests', () => {
}, },
}); });
render(reduxWrapper(<IntlProgressiveProfilingPage />)); render(reduxWrapper(<ProgressiveProfiling />));
expect(window.location.href).toBe(DASHBOARD_URL); expect(window.location.href).toBe(DASHBOARD_URL);
}); });
it('should redirect to provided redirect url', () => { it('should redirect to provided redirect url', async () => {
const redirectUrl = 'https://redirect-test.com'; const redirectUrl = 'https://redirect-test.com';
delete window.location; delete window.location;
window.location = { window.location = {
@@ -417,12 +436,17 @@ describe('ProgressiveProfilingTests', () => {
...initialState.welcomePage, ...initialState.welcomePage,
success: true, success: true,
}, },
cohesion: {
eventData,
},
}); });
render(reduxWrapper(<IntlProgressiveProfilingPage />)); render(reduxWrapper(<ProgressiveProfiling />));
const submitButton = screen.getByText('Submit'); const submitButton = screen.getByText('Submit');
fireEvent.click(submitButton); fireEvent.click(submitButton);
expect(window.location.href).toBe(redirectUrl); await waitFor(() => {
expect(window.location.href).toBe(redirectUrl);
}, { timeout: 1100 });
}); });
}); });
}); });

View File

@@ -1,13 +1,9 @@
import React from 'react'; import { IntlProvider } from '@edx/frontend-platform/i18n';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import { render } from '@testing-library/react'; import { render } from '@testing-library/react';
import SmallLayout from './SmallLayout'; import SmallLayout from './SmallLayout';
import mockedRecommendedProducts from '../data/tests/mockedData'; import mockedRecommendedProducts from '../data/tests/mockedData';
const IntlRecommendationsSmallLayoutPage = injectIntl(SmallLayout);
jest.mock('react-router-dom', () => ({ jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'), ...jest.requireActual('react-router-dom'),
useLocation: jest.fn(), useLocation: jest.fn(),
@@ -36,7 +32,7 @@ describe('RecommendationsPageTests', () => {
}); });
it('should render recommendations when recommendations are not loading', () => { it('should render recommendations when recommendations are not loading', () => {
const { container } = render(reduxWrapper(<IntlRecommendationsSmallLayoutPage {...props} />)); const { container } = render(reduxWrapper(<SmallLayout {...props} />));
const reactLoadingSkeleton = container.querySelector('.react-loading-skeleton'); const reactLoadingSkeleton = container.querySelector('.react-loading-skeleton');
@@ -48,7 +44,7 @@ describe('RecommendationsPageTests', () => {
...props, ...props,
isLoading: true, isLoading: true,
}; };
const { container } = render(reduxWrapper(<IntlRecommendationsSmallLayoutPage {...props} />)); const { container } = render(reduxWrapper(<SmallLayout {...props} />));
const reactLoadingSkeleton = container.querySelector('.react-loading-skeleton'); const reactLoadingSkeleton = container.querySelector('.react-loading-skeleton');

View File

@@ -1,4 +1,4 @@
import { renderHook } from '@testing-library/react-hooks'; import { renderHook } from '@testing-library/react';
import algoliasearchHelper from 'algoliasearch-helper'; import algoliasearchHelper from 'algoliasearch-helper';
import mockedRecommendedProducts from './mockedData'; import mockedRecommendedProducts from './mockedData';

View File

@@ -1,14 +1,12 @@
import React from 'react';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n'; import { IntlProvider } from '@edx/frontend-platform/i18n';
import { render } from '@testing-library/react'; import { render } from '@testing-library/react';
import configureStore from 'redux-mock-store'; import configureStore from 'redux-mock-store';
import mockedProductData from './mockedData'; import mockedProductData from './mockedData';
import RecommendationList from '../RecommendationsList'; import RecommendationList from '../RecommendationsList';
const IntlRecommendationList = injectIntl(RecommendationList);
const mockStore = configureStore(); const mockStore = configureStore();
describe('RecommendationsListTests', () => { describe('RecommendationsListTests', () => {
@@ -25,7 +23,7 @@ describe('RecommendationsListTests', () => {
userId: 1234567, userId: 1234567,
}; };
const { container } = render(reduxWrapper(<IntlRecommendationList {...props} />)); const { container } = render(reduxWrapper(<RecommendationList {...props} />));
const recommendationCards = container.querySelectorAll('.recommendation-card'); const recommendationCards = container.querySelectorAll('.recommendation-card');
expect(recommendationCards.length).toEqual(mockedProductData.length); expect(recommendationCards.length).toEqual(mockedProductData.length);
@@ -37,7 +35,7 @@ describe('RecommendationsListTests', () => {
userId: 1234567, userId: 1234567,
}; };
const { getByText } = render(reduxWrapper(<IntlRecommendationList {...props} />)); const { getByText } = render(reduxWrapper(<RecommendationList {...props} />));
const firstFooterContent = getByText('1 Course'); const firstFooterContent = getByText('1 Course');
const secondFooterContent = getByText('2 Courses'); const secondFooterContent = getByText('2 Courses');

View File

@@ -1,9 +1,8 @@
import React from 'react';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { getConfig } from '@edx/frontend-platform'; import { getConfig } from '@edx/frontend-platform';
import { sendTrackEvent } from '@edx/frontend-platform/analytics'; import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n'; import { IntlProvider } from '@edx/frontend-platform/i18n';
import { useMediaQuery } from '@openedx/paragon'; import { useMediaQuery } from '@openedx/paragon';
import { fireEvent, render } from '@testing-library/react'; import { fireEvent, render } from '@testing-library/react';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
@@ -16,7 +15,6 @@ import mockedRecommendedProducts from '../data/tests/mockedData';
import RecommendationsPage from '../RecommendationsPage'; import RecommendationsPage from '../RecommendationsPage';
import { eventNames, getProductMapping } from '../track'; import { eventNames, getProductMapping } from '../track';
const IntlRecommendationsPage = injectIntl(RecommendationsPage);
const mockStore = configureStore(); const mockStore = configureStore();
jest.mock('@edx/frontend-platform/analytics', () => ({ jest.mock('@edx/frontend-platform/analytics', () => ({
@@ -77,7 +75,7 @@ describe('RecommendationsPageTests', () => {
}); });
it('should redirect to dashboard if user is not coming from registration workflow', () => { it('should redirect to dashboard if user is not coming from registration workflow', () => {
render(reduxWrapper(<IntlRecommendationsPage />)); render(reduxWrapper(<RecommendationsPage />));
expect(window.location.href).toEqual(dashboardUrl); expect(window.location.href).toEqual(dashboardUrl);
}); });
@@ -86,14 +84,14 @@ describe('RecommendationsPageTests', () => {
recommendations: [], recommendations: [],
isLoading: false, isLoading: false,
}); });
render(reduxWrapper(<IntlRecommendationsPage />)); render(reduxWrapper(<RecommendationsPage />));
expect(window.location.href).toEqual(dashboardUrl); expect(window.location.href).toEqual(dashboardUrl);
}); });
it('should redirect user if they click "Skip for now" button', () => { it('should redirect user if they click "Skip for now" button', () => {
mockUseLocation(); mockUseLocation();
jest.useFakeTimers(); jest.useFakeTimers();
const { container } = render(reduxWrapper(<IntlRecommendationsPage />)); const { container } = render(reduxWrapper(<RecommendationsPage />));
const skipButton = container.querySelector('.pgn__stateful-btn-state-default'); const skipButton = container.querySelector('.pgn__stateful-btn-state-default');
fireEvent.click(skipButton); fireEvent.click(skipButton);
jest.advanceTimersByTime(300); jest.advanceTimersByTime(300);
@@ -103,7 +101,7 @@ describe('RecommendationsPageTests', () => {
it('should display recommendations small layout for small screen', () => { it('should display recommendations small layout for small screen', () => {
mockUseLocation(); mockUseLocation();
useMediaQuery.mockReturnValue(true); useMediaQuery.mockReturnValue(true);
const { container } = render(reduxWrapper(<IntlRecommendationsPage />)); const { container } = render(reduxWrapper(<RecommendationsPage />));
const recommendationsSmallLayout = container.querySelector('#recommendations-small-layout'); const recommendationsSmallLayout = container.querySelector('#recommendations-small-layout');
const reactLoadingSkeleton = container.querySelector('.react-loading-skeleton'); const reactLoadingSkeleton = container.querySelector('.react-loading-skeleton');
@@ -115,7 +113,7 @@ describe('RecommendationsPageTests', () => {
it('should display recommendations large layout for large screen', () => { it('should display recommendations large layout for large screen', () => {
mockUseLocation(); mockUseLocation();
useMediaQuery.mockReturnValue(false); useMediaQuery.mockReturnValue(false);
const { container } = render(reduxWrapper(<IntlRecommendationsPage />)); const { container } = render(reduxWrapper(<RecommendationsPage />));
const pgnCollapsible = container.querySelector('.pgn_collapsible'); const pgnCollapsible = container.querySelector('.pgn_collapsible');
const reactLoadingSkeleton = container.querySelector('.react-loading-skeleton'); const reactLoadingSkeleton = container.querySelector('.react-loading-skeleton');
@@ -131,7 +129,7 @@ describe('RecommendationsPageTests', () => {
recommendations: [], recommendations: [],
isLoading: true, isLoading: true,
}); });
const { container } = render(reduxWrapper(<IntlRecommendationsPage />)); const { container } = render(reduxWrapper(<RecommendationsPage />));
const reactLoadingSkeleton = container.querySelector('.react-loading-skeleton'); const reactLoadingSkeleton = container.querySelector('.react-loading-skeleton');
@@ -145,7 +143,7 @@ describe('RecommendationsPageTests', () => {
recommendations: [], recommendations: [],
isLoading: true, isLoading: true,
}); });
const { container } = render(reduxWrapper(<IntlRecommendationsPage />)); const { container } = render(reduxWrapper(<RecommendationsPage />));
const reactLoadingSkeleton = container.querySelector('.react-loading-skeleton'); const reactLoadingSkeleton = container.querySelector('.react-loading-skeleton');
@@ -160,7 +158,7 @@ describe('RecommendationsPageTests', () => {
}); });
useMediaQuery.mockReturnValue(false); useMediaQuery.mockReturnValue(false);
render(reduxWrapper(<IntlRecommendationsPage />)); render(reduxWrapper(<RecommendationsPage />));
expect(sendTrackEvent).toBeCalled(); expect(sendTrackEvent).toBeCalled();
expect(sendTrackEvent).toHaveBeenCalledWith( expect(sendTrackEvent).toHaveBeenCalledWith(

View File

@@ -15,7 +15,7 @@ const generateProductKey = (product) => (
export const getProductMapping = (recommendedProducts) => recommendedProducts.map((product) => ({ export const getProductMapping = (recommendedProducts) => recommendedProducts.map((product) => ({
product_key: generateProductKey(product), product_key: generateProductKey(product),
product_line: product.cardType, product_line: product.cardType,
product_source: product.productSource.name, product_source: product?.productSource?.name,
})); }));
export const trackRecommendationClick = (product, position, userId) => { export const trackRecommendationClick = (product, position, userId) => {
@@ -25,7 +25,7 @@ export const trackRecommendationClick = (product, position, userId) => {
recommendation_type: product.recommendationType, recommendation_type: product.recommendationType,
product_key: generateProductKey(product), product_key: generateProductKey(product),
product_line: product.cardType, product_line: product.cardType,
product_source: product.productSource.name, product_source: product?.productSource?.name,
user_id: userId, user_id: userId,
}); });

View File

@@ -97,7 +97,7 @@ const CountryField = (props) => {
}; };
const getCountryList = () => countryList.map((country) => ( const getCountryList = () => countryList.map((country) => (
<FormAutosuggestOption key={country[COUNTRY_CODE_KEY]} id={country[COUNTRY_CODE_KEY]}> <FormAutosuggestOption key={country[COUNTRY_DISPLAY_KEY]} id={country[COUNTRY_CODE_KEY]}>
{country[COUNTRY_DISPLAY_KEY]} {country[COUNTRY_DISPLAY_KEY]}
</FormAutosuggestOption> </FormAutosuggestOption>
)); ));

View File

@@ -1,8 +1,7 @@
import React from 'react';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { mergeConfig } from '@edx/frontend-platform'; import { mergeConfig } from '@edx/frontend-platform';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n'; import { IntlProvider } from '@edx/frontend-platform/i18n';
import { fireEvent, render } from '@testing-library/react'; import { fireEvent, render } from '@testing-library/react';
import { BrowserRouter as Router } from 'react-router-dom'; import { BrowserRouter as Router } from 'react-router-dom';
import configureStore from 'redux-mock-store'; import configureStore from 'redux-mock-store';
@@ -10,7 +9,6 @@ import configureStore from 'redux-mock-store';
import { COUNTRY_CODE_KEY, COUNTRY_DISPLAY_KEY } from './validator'; import { COUNTRY_CODE_KEY, COUNTRY_DISPLAY_KEY } from './validator';
import { CountryField } from '../index'; import { CountryField } from '../index';
const IntlCountryField = injectIntl(CountryField);
const mockStore = configureStore(); const mockStore = configureStore();
jest.mock('react-router-dom', () => { jest.mock('react-router-dom', () => {
@@ -82,7 +80,7 @@ describe('CountryField', () => {
}; };
it('should run country field validation when onBlur is fired', () => { it('should run country field validation when onBlur is fired', () => {
const { container } = render(routerWrapper(reduxWrapper(<IntlCountryField {...props} />))); const { container } = render(routerWrapper(reduxWrapper(<CountryField {...props} />)));
const countryInput = container.querySelector('input[name="country"]'); const countryInput = container.querySelector('input[name="country"]');
fireEvent.blur(countryInput, { fireEvent.blur(countryInput, {
@@ -97,7 +95,7 @@ describe('CountryField', () => {
}); });
it('should run country field validation when country name is invalid', () => { it('should run country field validation when country name is invalid', () => {
const { container } = render(routerWrapper(reduxWrapper(<IntlCountryField {...props} />))); const { container } = render(routerWrapper(reduxWrapper(<CountryField {...props} />)));
const countryInput = container.querySelector('input[name="country"]'); const countryInput = container.querySelector('input[name="country"]');
fireEvent.blur(countryInput, { fireEvent.blur(countryInput, {
@@ -112,7 +110,7 @@ describe('CountryField', () => {
}); });
it('should not run country field validation when onBlur is fired by drop-down arrow icon click', () => { it('should not run country field validation when onBlur is fired by drop-down arrow icon click', () => {
const { container } = render(routerWrapper(reduxWrapper(<IntlCountryField {...props} />))); const { container } = render(routerWrapper(reduxWrapper(<CountryField {...props} />)));
const countryInput = container.querySelector('input[name="country"]'); const countryInput = container.querySelector('input[name="country"]');
const dropdownArrowIcon = container.querySelector('.btn-icon.pgn__form-autosuggest__icon-button'); const dropdownArrowIcon = container.querySelector('.btn-icon.pgn__form-autosuggest__icon-button');
@@ -125,7 +123,7 @@ describe('CountryField', () => {
}); });
it('should update errors for frontend validations', () => { it('should update errors for frontend validations', () => {
const { container } = render(routerWrapper(reduxWrapper(<IntlCountryField {...props} />))); const { container } = render(routerWrapper(reduxWrapper(<CountryField {...props} />)));
const countryInput = container.querySelector('input[name="country"]'); const countryInput = container.querySelector('input[name="country"]');
fireEvent.blur(countryInput, { target: { value: '', name: 'country' } }); fireEvent.blur(countryInput, { target: { value: '', name: 'country' } });
@@ -135,7 +133,7 @@ describe('CountryField', () => {
}); });
it('should clear error on focus', () => { it('should clear error on focus', () => {
const { container } = render(routerWrapper(reduxWrapper(<IntlCountryField {...props} />))); const { container } = render(routerWrapper(reduxWrapper(<CountryField {...props} />)));
const countryInput = container.querySelector('input[name="country"]'); const countryInput = container.querySelector('input[name="country"]');
fireEvent.focus(countryInput); fireEvent.focus(countryInput);
@@ -153,7 +151,7 @@ describe('CountryField', () => {
}, },
}); });
const { container } = render(routerWrapper(reduxWrapper(<IntlCountryField {...props} />))); const { container } = render(routerWrapper(reduxWrapper(<CountryField {...props} />)));
container.querySelector('input[name="country"]'); container.querySelector('input[name="country"]');
expect(props.onChangeHandler).toHaveBeenCalledTimes(1); expect(props.onChangeHandler).toHaveBeenCalledTimes(1);
@@ -164,7 +162,7 @@ describe('CountryField', () => {
}); });
it('should set option on dropdown menu item click', () => { it('should set option on dropdown menu item click', () => {
const { container } = render(routerWrapper(reduxWrapper(<IntlCountryField {...props} />))); const { container } = render(routerWrapper(reduxWrapper(<CountryField {...props} />)));
const dropdownButton = container.querySelector('.pgn__form-autosuggest__icon-button'); const dropdownButton = container.querySelector('.pgn__form-autosuggest__icon-button');
fireEvent.click(dropdownButton); fireEvent.click(dropdownButton);
@@ -181,7 +179,7 @@ describe('CountryField', () => {
it('should set value on change', () => { it('should set value on change', () => {
const { container } = render( const { container } = render(
routerWrapper(reduxWrapper(<IntlCountryField {...props} />)), routerWrapper(reduxWrapper(<CountryField {...props} />)),
); );
const countryInput = container.querySelector('input[name="country"]'); const countryInput = container.querySelector('input[name="country"]');
@@ -200,7 +198,7 @@ describe('CountryField', () => {
errorMessage: 'country error message', errorMessage: 'country error message',
}; };
const { container } = render(routerWrapper(reduxWrapper(<IntlCountryField {...props} />))); const { container } = render(routerWrapper(reduxWrapper(<CountryField {...props} />)));
const feedbackElement = container.querySelector('div[feedback-for="country"]'); const feedbackElement = container.querySelector('div[feedback-for="country"]');
expect(feedbackElement).toBeTruthy(); expect(feedbackElement).toBeTruthy();

View File

@@ -1,8 +1,7 @@
import React from 'react';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { getConfig } from '@edx/frontend-platform'; import { getConfig } from '@edx/frontend-platform';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n'; import { IntlProvider } from '@edx/frontend-platform/i18n';
import { fireEvent, render } from '@testing-library/react'; import { fireEvent, render } from '@testing-library/react';
import { BrowserRouter as Router } from 'react-router-dom'; import { BrowserRouter as Router } from 'react-router-dom';
import configureStore from 'redux-mock-store'; import configureStore from 'redux-mock-store';
@@ -10,7 +9,6 @@ import configureStore from 'redux-mock-store';
import { clearRegistrationBackendError, fetchRealtimeValidations } from '../../data/actions'; import { clearRegistrationBackendError, fetchRealtimeValidations } from '../../data/actions';
import { EmailField } from '../index'; import { EmailField } from '../index';
const IntlEmailField = injectIntl(EmailField);
const mockStore = configureStore(); const mockStore = configureStore();
jest.mock('react-router-dom', () => { jest.mock('react-router-dom', () => {
@@ -80,7 +78,7 @@ describe('EmailField', () => {
}; };
it('should run email field validation when onBlur is fired', () => { it('should run email field validation when onBlur is fired', () => {
const { container } = render(routerWrapper(reduxWrapper(<IntlEmailField {...props} />))); const { container } = render(routerWrapper(reduxWrapper(<EmailField {...props} />)));
const emailInput = container.querySelector('input#email'); const emailInput = container.querySelector('input#email');
fireEvent.blur(emailInput, { target: { value: '', name: 'email' } }); fireEvent.blur(emailInput, { target: { value: '', name: 'email' } });
@@ -92,7 +90,7 @@ describe('EmailField', () => {
}); });
it('should update errors for frontend validations', () => { it('should update errors for frontend validations', () => {
const { container } = render(routerWrapper(reduxWrapper(<IntlEmailField {...props} />))); const { container } = render(routerWrapper(reduxWrapper(<EmailField {...props} />)));
const emailInput = container.querySelector('input#email'); const emailInput = container.querySelector('input#email');
fireEvent.blur(emailInput, { target: { value: 'ab', name: 'email' } }); fireEvent.blur(emailInput, { target: { value: 'ab', name: 'email' } });
@@ -105,7 +103,7 @@ describe('EmailField', () => {
}); });
it('should clear error on focus', () => { it('should clear error on focus', () => {
const { container } = render(routerWrapper(reduxWrapper(<IntlEmailField {...props} />))); const { container } = render(routerWrapper(reduxWrapper(<EmailField {...props} />)));
const emailInput = container.querySelector('input#email'); const emailInput = container.querySelector('input#email');
fireEvent.focus(emailInput, { target: { value: '', name: 'email' } }); fireEvent.focus(emailInput, { target: { value: '', name: 'email' } });
@@ -119,7 +117,7 @@ describe('EmailField', () => {
it('should call backend validation api on blur event, if frontend validations have passed', () => { it('should call backend validation api on blur event, if frontend validations have passed', () => {
store.dispatch = jest.fn(store.dispatch); store.dispatch = jest.fn(store.dispatch);
const { container } = render(routerWrapper(reduxWrapper(<IntlEmailField {...props} />))); const { container } = render(routerWrapper(reduxWrapper(<EmailField {...props} />)));
// Enter a valid email so that frontend validations are passed // Enter a valid email so that frontend validations are passed
const emailInput = container.querySelector('input#email'); const emailInput = container.querySelector('input#email');
@@ -129,7 +127,7 @@ describe('EmailField', () => {
}); });
it('should give email suggestions for common service provider domain typos', () => { it('should give email suggestions for common service provider domain typos', () => {
const { container } = render(routerWrapper(reduxWrapper(<IntlEmailField {...props} />))); const { container } = render(routerWrapper(reduxWrapper(<EmailField {...props} />)));
const emailInput = container.querySelector('input#email'); const emailInput = container.querySelector('input#email');
fireEvent.blur(emailInput, { target: { value: 'john@yopmail.com', name: 'email' } }); fireEvent.blur(emailInput, { target: { value: 'john@yopmail.com', name: 'email' } });
@@ -139,7 +137,7 @@ describe('EmailField', () => {
}); });
it('should be able to click on email suggestions and set it as value', () => { it('should be able to click on email suggestions and set it as value', () => {
const { container } = render(routerWrapper(reduxWrapper(<IntlEmailField {...props} />))); const { container } = render(routerWrapper(reduxWrapper(<EmailField {...props} />)));
const emailInput = container.querySelector('input#email'); const emailInput = container.querySelector('input#email');
fireEvent.blur(emailInput, { target: { value: 'john@yopmail.com', name: 'email' } }); fireEvent.blur(emailInput, { target: { value: 'john@yopmail.com', name: 'email' } });
@@ -154,7 +152,7 @@ describe('EmailField', () => {
}); });
it('should give error for common top level domain mistakes', () => { it('should give error for common top level domain mistakes', () => {
const { container } = render(routerWrapper(reduxWrapper(<IntlEmailField {...props} />))); const { container } = render(routerWrapper(reduxWrapper(<EmailField {...props} />)));
const emailInput = container.querySelector('input#email'); const emailInput = container.querySelector('input#email');
fireEvent.blur(emailInput, { target: { value: 'john@gmail.mistake', name: 'email' } }); fireEvent.blur(emailInput, { target: { value: 'john@gmail.mistake', name: 'email' } });
@@ -164,7 +162,7 @@ describe('EmailField', () => {
}); });
it('should give error and suggestion for invalid email', () => { it('should give error and suggestion for invalid email', () => {
const { container } = render(routerWrapper(reduxWrapper(<IntlEmailField {...props} />))); const { container } = render(routerWrapper(reduxWrapper(<EmailField {...props} />)));
const emailInput = container.querySelector('input#email'); const emailInput = container.querySelector('input#email');
fireEvent.blur(emailInput, { target: { value: 'john@gmail', name: 'email' } }); fireEvent.blur(emailInput, { target: { value: 'john@gmail', name: 'email' } });
@@ -194,7 +192,7 @@ describe('EmailField', () => {
store.dispatch = jest.fn(store.dispatch); store.dispatch = jest.fn(store.dispatch);
const { container } = render(routerWrapper(reduxWrapper(<IntlEmailField {...props} />))); const { container } = render(routerWrapper(reduxWrapper(<EmailField {...props} />)));
const emailInput = container.querySelector('input#email'); const emailInput = container.querySelector('input#email');
fireEvent.focus(emailInput, { target: { value: 'a@gmail.com', name: 'email' } }); fireEvent.focus(emailInput, { target: { value: 'a@gmail.com', name: 'email' } });
@@ -203,7 +201,7 @@ describe('EmailField', () => {
}); });
it('should clear email suggestions when close icon is clicked', () => { it('should clear email suggestions when close icon is clicked', () => {
const { container } = render(routerWrapper(reduxWrapper(<IntlEmailField {...props} />))); const { container } = render(routerWrapper(reduxWrapper(<EmailField {...props} />)));
const emailInput = container.querySelector('input#email'); const emailInput = container.querySelector('input#email');
fireEvent.blur(emailInput, { target: { value: 'john@gmail.mistake', name: 'email' } }); fireEvent.blur(emailInput, { target: { value: 'john@gmail.mistake', name: 'email' } });
@@ -224,7 +222,7 @@ describe('EmailField', () => {
confirmEmailValue: 'confirmEmail@yopmail.com', confirmEmailValue: 'confirmEmail@yopmail.com',
}; };
const { container } = render(routerWrapper(reduxWrapper(<IntlEmailField {...props} />))); const { container } = render(routerWrapper(reduxWrapper(<EmailField {...props} />)));
const emailInput = container.querySelector('input#email'); const emailInput = container.querySelector('input#email');
fireEvent.blur(emailInput, { target: { value: 'differentEmail@yopmail.com', name: 'email' } }); fireEvent.blur(emailInput, { target: { value: 'differentEmail@yopmail.com', name: 'email' } });

View File

@@ -1,13 +1,9 @@
import React from 'react';
import { getConfig, mergeConfig } from '@edx/frontend-platform'; import { getConfig, mergeConfig } from '@edx/frontend-platform';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n'; import { IntlProvider } from '@edx/frontend-platform/i18n';
import { render } from '@testing-library/react'; import { render } from '@testing-library/react';
import { HonorCode } from '../index'; import { HonorCode } from '../index';
const IntlHonorCode = injectIntl(HonorCode);
describe('HonorCodeTest', () => { describe('HonorCodeTest', () => {
mergeConfig({ mergeConfig({
PRIVACY_POLICY: 'http://privacy-policy.com', PRIVACY_POLICY: 'http://privacy-policy.com',
@@ -28,7 +24,7 @@ describe('HonorCodeTest', () => {
const errorMessage = `You must agree to the ${getConfig().SITE_NAME} Honor Code`; const errorMessage = `You must agree to the ${getConfig().SITE_NAME} Honor Code`;
const { container } = render( const { container } = render(
<IntlProvider locale="en"> <IntlProvider locale="en">
<IntlHonorCode <HonorCode
errorMessage={errorMessage} errorMessage={errorMessage}
onChangeHandler={changeHandler} onChangeHandler={changeHandler}
/> />
@@ -43,7 +39,7 @@ describe('HonorCodeTest', () => {
const expectedMsg = 'I agree to the Your Platform Name Here\u00a0Honor Codein a new tab'; const expectedMsg = 'I agree to the Your Platform Name Here\u00a0Honor Codein a new tab';
const { container } = render( const { container } = render(
<IntlProvider locale="en"> <IntlProvider locale="en">
<IntlHonorCode onChangeHandler={changeHandler} /> <HonorCode onChangeHandler={changeHandler} />
</IntlProvider>, </IntlProvider>,
); );
@@ -56,7 +52,7 @@ describe('HonorCodeTest', () => {
it('should render Terms of Service and Honor code field', () => { it('should render Terms of Service and Honor code field', () => {
const { container } = render( const { container } = render(
<IntlProvider locale="en"> <IntlProvider locale="en">
<IntlHonorCode fieldType="tos_and_honor_code" onChangeHandler={changeHandler} /> <HonorCode fieldType="tos_and_honor_code" onChangeHandler={changeHandler} />
</IntlProvider>, </IntlProvider>,
); );
const expectedMsg = 'By creating an account, you agree to the Terms of Service and Honor Code and you ' const expectedMsg = 'By creating an account, you agree to the Terms of Service and Honor Code and you '

View File

@@ -1,7 +1,6 @@
import React from 'react';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n'; import { IntlProvider } from '@edx/frontend-platform/i18n';
import { fireEvent, render } from '@testing-library/react'; import { fireEvent, render } from '@testing-library/react';
import { BrowserRouter as Router } from 'react-router-dom'; import { BrowserRouter as Router } from 'react-router-dom';
import configureStore from 'redux-mock-store'; import configureStore from 'redux-mock-store';
@@ -9,7 +8,6 @@ import configureStore from 'redux-mock-store';
import { clearRegistrationBackendError, fetchRealtimeValidations } from '../../data/actions'; import { clearRegistrationBackendError, fetchRealtimeValidations } from '../../data/actions';
import { NameField } from '../index'; import { NameField } from '../index';
const IntlNameField = injectIntl(NameField);
const mockStore = configureStore(); const mockStore = configureStore();
jest.mock('react-router-dom', () => { jest.mock('react-router-dom', () => {
@@ -69,7 +67,7 @@ describe('NameField', () => {
const fieldValidation = { name: 'Enter your full name' }; const fieldValidation = { name: 'Enter your full name' };
it('should run name field validation when onBlur is fired', () => { it('should run name field validation when onBlur is fired', () => {
const { container } = render(routerWrapper(reduxWrapper(<IntlNameField {...props} />))); const { container } = render(routerWrapper(reduxWrapper(<NameField {...props} />)));
const nameInput = container.querySelector('input#name'); const nameInput = container.querySelector('input#name');
fireEvent.blur(nameInput, { target: { value: '', name: 'name' } }); fireEvent.blur(nameInput, { target: { value: '', name: 'name' } });
@@ -82,7 +80,7 @@ describe('NameField', () => {
}); });
it('should update errors for frontend validations', () => { it('should update errors for frontend validations', () => {
const { container } = render(routerWrapper(reduxWrapper(<IntlNameField {...props} />))); const { container } = render(routerWrapper(reduxWrapper(<NameField {...props} />)));
const nameInput = container.querySelector('input#name'); const nameInput = container.querySelector('input#name');
fireEvent.blur(nameInput, { target: { value: 'https://invalid-name.com', name: 'name' } }); fireEvent.blur(nameInput, { target: { value: 'https://invalid-name.com', name: 'name' } });
@@ -95,7 +93,7 @@ describe('NameField', () => {
}); });
it('should clear error on focus', () => { it('should clear error on focus', () => {
const { container } = render(routerWrapper(reduxWrapper(<IntlNameField {...props} />))); const { container } = render(routerWrapper(reduxWrapper(<NameField {...props} />)));
const nameInput = container.querySelector('input#name'); const nameInput = container.querySelector('input#name');
fireEvent.focus(nameInput, { target: { value: '', name: 'name' } }); fireEvent.focus(nameInput, { target: { value: '', name: 'name' } });
@@ -113,7 +111,7 @@ describe('NameField', () => {
...props, ...props,
shouldFetchUsernameSuggestions: true, shouldFetchUsernameSuggestions: true,
}; };
const { container } = render(routerWrapper(reduxWrapper(<IntlNameField {...props} />))); const { container } = render(routerWrapper(reduxWrapper(<NameField {...props} />)));
const nameInput = container.querySelector('input#name'); const nameInput = container.querySelector('input#name');
// Enter a valid name so that frontend validations are passed // Enter a valid name so that frontend validations are passed
@@ -135,7 +133,7 @@ describe('NameField', () => {
}); });
store.dispatch = jest.fn(store.dispatch); store.dispatch = jest.fn(store.dispatch);
const { container } = render(routerWrapper(reduxWrapper(<IntlNameField {...props} />))); const { container } = render(routerWrapper(reduxWrapper(<NameField {...props} />)));
const nameInput = container.querySelector('input#name'); const nameInput = container.querySelector('input#name');

View File

@@ -11,7 +11,7 @@ export const INVALID_NAME_REGEX = /https?:\/\/(?:[-\w.]|(?:%[\da-fA-F]{2}))*/g;
const validateName = (value, formatMessage) => { const validateName = (value, formatMessage) => {
let fieldError = ''; let fieldError = '';
if (!value.trim()) { if (!value || (value && !value.trim())) {
fieldError = formatMessage(messages['empty.name.field.error']); fieldError = formatMessage(messages['empty.name.field.error']);
} else if (URL_REGEX.test(value) || HTML_REGEX.test(value) || INVALID_NAME_REGEX.test(value)) { } else if (URL_REGEX.test(value) || HTML_REGEX.test(value) || INVALID_NAME_REGEX.test(value)) {
fieldError = formatMessage(messages['name.validation.message']); fieldError = formatMessage(messages['name.validation.message']);

View File

@@ -1,13 +1,9 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform'; import { getConfig } from '@edx/frontend-platform';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n'; import { IntlProvider } from '@edx/frontend-platform/i18n';
import { fireEvent, render } from '@testing-library/react'; import { fireEvent, render } from '@testing-library/react';
import { TermsOfService } from '../index'; import { TermsOfService } from '../index';
const IntlTermsOfService = injectIntl(TermsOfService);
describe('TermsOfServiceTest', () => { describe('TermsOfServiceTest', () => {
let value = false; let value = false;
@@ -23,7 +19,7 @@ describe('TermsOfServiceTest', () => {
const errorMessage = `You must agree to the ${getConfig().SITE_NAME} Terms of Service`; const errorMessage = `You must agree to the ${getConfig().SITE_NAME} Terms of Service`;
const { container } = render( const { container } = render(
<IntlProvider locale="en"> <IntlProvider locale="en">
<IntlTermsOfService errorMessage={errorMessage} onChangeHandler={changeHandler} /> <TermsOfService errorMessage={errorMessage} onChangeHandler={changeHandler} />
</IntlProvider>, </IntlProvider>,
); );
const errorElement = container.querySelector('.form-text-size'); const errorElement = container.querySelector('.form-text-size');
@@ -33,7 +29,7 @@ describe('TermsOfServiceTest', () => {
it('should render Terms of Service field', () => { it('should render Terms of Service field', () => {
const { container } = render( const { container } = render(
<IntlProvider locale="en"> <IntlProvider locale="en">
<IntlTermsOfService onChangeHandler={changeHandler} /> <TermsOfService onChangeHandler={changeHandler} />
</IntlProvider>, </IntlProvider>,
); );
@@ -48,7 +44,7 @@ describe('TermsOfServiceTest', () => {
it('should change value when Terms of Service field is checked', () => { it('should change value when Terms of Service field is checked', () => {
const { container } = render( const { container } = render(
<IntlProvider locale="en"> <IntlProvider locale="en">
<IntlTermsOfService onChangeHandler={changeHandler} /> <TermsOfService onChangeHandler={changeHandler} />
</IntlProvider>, </IntlProvider>,
); );
const field = container.querySelector('input#tos'); const field = container.querySelector('input#tos');

View File

@@ -1,7 +1,6 @@
import React from 'react';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n'; import { IntlProvider } from '@edx/frontend-platform/i18n';
import { fireEvent, render } from '@testing-library/react'; import { fireEvent, render } from '@testing-library/react';
import { BrowserRouter as Router } from 'react-router-dom'; import { BrowserRouter as Router } from 'react-router-dom';
import configureStore from 'redux-mock-store'; import configureStore from 'redux-mock-store';
@@ -9,7 +8,6 @@ import configureStore from 'redux-mock-store';
import { clearRegistrationBackendError, clearUsernameSuggestions, fetchRealtimeValidations } from '../../data/actions'; import { clearRegistrationBackendError, clearUsernameSuggestions, fetchRealtimeValidations } from '../../data/actions';
import { UsernameField } from '../index'; import { UsernameField } from '../index';
const IntlUsernameField = injectIntl(UsernameField);
const mockStore = configureStore(); const mockStore = configureStore();
jest.mock('react-router-dom', () => { jest.mock('react-router-dom', () => {
@@ -73,7 +71,7 @@ describe('UsernameField', () => {
}; };
it('should run username field validation when onBlur is fired', () => { it('should run username field validation when onBlur is fired', () => {
const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />))); const { container } = render(routerWrapper(reduxWrapper(<UsernameField {...props} />)));
const usernameField = container.querySelector('input#username'); const usernameField = container.querySelector('input#username');
fireEvent.blur(usernameField, { target: { value: '', name: 'username' } }); fireEvent.blur(usernameField, { target: { value: '', name: 'username' } });
@@ -86,7 +84,7 @@ describe('UsernameField', () => {
}); });
it('should update errors for frontend validations', () => { it('should update errors for frontend validations', () => {
const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />))); const { container } = render(routerWrapper(reduxWrapper(<UsernameField {...props} />)));
const usernameField = container.querySelector('input#username'); const usernameField = container.querySelector('input#username');
fireEvent.blur(usernameField, { target: { value: 'user#', name: 'username' } }); fireEvent.blur(usernameField, { target: { value: 'user#', name: 'username' } });
@@ -99,7 +97,7 @@ describe('UsernameField', () => {
}); });
it('should clear error on focus', () => { it('should clear error on focus', () => {
const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />))); const { container } = render(routerWrapper(reduxWrapper(<UsernameField {...props} />)));
const usernameField = container.querySelector('input#username'); const usernameField = container.querySelector('input#username');
fireEvent.focus(usernameField, { target: { value: '', name: 'username' } }); fireEvent.focus(usernameField, { target: { value: '', name: 'username' } });
@@ -112,7 +110,7 @@ describe('UsernameField', () => {
}); });
it('should remove space from field on focus if space exists', () => { it('should remove space from field on focus if space exists', () => {
const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />))); const { container } = render(routerWrapper(reduxWrapper(<UsernameField {...props} />)));
const usernameField = container.querySelector('input#username'); const usernameField = container.querySelector('input#username');
fireEvent.focus(usernameField, { target: { value: ' ', name: 'username' } }); fireEvent.focus(usernameField, { target: { value: ' ', name: 'username' } });
@@ -125,7 +123,7 @@ describe('UsernameField', () => {
it('should call backend validation api on blur event, if frontend validations have passed', () => { it('should call backend validation api on blur event, if frontend validations have passed', () => {
store.dispatch = jest.fn(store.dispatch); store.dispatch = jest.fn(store.dispatch);
const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />))); const { container } = render(routerWrapper(reduxWrapper(<UsernameField {...props} />)));
const usernameField = container.querySelector('input#username'); const usernameField = container.querySelector('input#username');
// Enter a valid username so that frontend validations are passed // Enter a valid username so that frontend validations are passed
@@ -135,7 +133,7 @@ describe('UsernameField', () => {
}); });
it('should remove space from the start of username on change', () => { it('should remove space from the start of username on change', () => {
const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />))); const { container } = render(routerWrapper(reduxWrapper(<UsernameField {...props} />)));
const usernameField = container.querySelector('input#username'); const usernameField = container.querySelector('input#username');
fireEvent.change(usernameField, { target: { value: ' test-user', name: 'username' } }); fireEvent.change(usernameField, { target: { value: ' test-user', name: 'username' } });
@@ -146,7 +144,7 @@ describe('UsernameField', () => {
}); });
it('should not set username if it is more than 30 character long', () => { it('should not set username if it is more than 30 character long', () => {
const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />))); const { container } = render(routerWrapper(reduxWrapper(<UsernameField {...props} />)));
const usernameField = container.querySelector('input#username'); const usernameField = container.querySelector('input#username');
fireEvent.change(usernameField, { target: { value: 'why_this_is_not_valid_username_', name: 'username' } }); fireEvent.change(usernameField, { target: { value: 'why_this_is_not_valid_username_', name: 'username' } });
@@ -157,7 +155,7 @@ describe('UsernameField', () => {
it('should clear username suggestions when username field is focused in', () => { it('should clear username suggestions when username field is focused in', () => {
store.dispatch = jest.fn(store.dispatch); store.dispatch = jest.fn(store.dispatch);
const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />))); const { container } = render(routerWrapper(reduxWrapper(<UsernameField {...props} />)));
const usernameField = container.querySelector('input#username'); const usernameField = container.querySelector('input#username');
fireEvent.focus(usernameField); fireEvent.focus(usernameField);
@@ -179,7 +177,7 @@ describe('UsernameField', () => {
errorMessage: 'It looks like this username is already taken', errorMessage: 'It looks like this username is already taken',
}; };
const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />))); const { container } = render(routerWrapper(reduxWrapper(<UsernameField {...props} />)));
const usernameSuggestions = container.querySelectorAll('button.username-suggestions--chip'); const usernameSuggestions = container.querySelectorAll('button.username-suggestions--chip');
expect(usernameSuggestions.length).toEqual(3); expect(usernameSuggestions.length).toEqual(3);
}); });
@@ -198,7 +196,7 @@ describe('UsernameField', () => {
value: ' ', value: ' ',
}; };
const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />))); const { container } = render(routerWrapper(reduxWrapper(<UsernameField {...props} />)));
const usernameSuggestions = container.querySelectorAll('button.username-suggestions--chip'); const usernameSuggestions = container.querySelectorAll('button.username-suggestions--chip');
expect(usernameSuggestions.length).toEqual(3); expect(usernameSuggestions.length).toEqual(3);
}); });
@@ -218,7 +216,7 @@ describe('UsernameField', () => {
errorMessage: 'username error', errorMessage: 'username error',
}; };
const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />))); const { container } = render(routerWrapper(reduxWrapper(<UsernameField {...props} />)));
const usernameSuggestions = container.querySelectorAll('button.username-suggestions--chip'); const usernameSuggestions = container.querySelectorAll('button.username-suggestions--chip');
expect(usernameSuggestions.length).toEqual(3); expect(usernameSuggestions.length).toEqual(3);
}); });
@@ -232,7 +230,7 @@ describe('UsernameField', () => {
}, },
}); });
render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />))); render(routerWrapper(reduxWrapper(<UsernameField {...props} />)));
expect(props.handleChange).toHaveBeenCalledTimes(1); expect(props.handleChange).toHaveBeenCalledTimes(1);
expect(props.handleChange).toHaveBeenCalledWith( expect(props.handleChange).toHaveBeenCalledWith(
{ target: { name: 'username', value: ' ' } }, { target: { name: 'username', value: ' ' } },
@@ -253,7 +251,7 @@ describe('UsernameField', () => {
value: ' ', value: ' ',
}; };
const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />))); const { container } = render(routerWrapper(reduxWrapper(<UsernameField {...props} />)));
const usernameSuggestion = container.querySelector('.username-suggestions--chip'); const usernameSuggestion = container.querySelector('.username-suggestions--chip');
fireEvent.click(usernameSuggestion); fireEvent.click(usernameSuggestion);
expect(props.handleChange).toHaveBeenCalledTimes(1); expect(props.handleChange).toHaveBeenCalledTimes(1);
@@ -277,7 +275,7 @@ describe('UsernameField', () => {
value: ' ', value: ' ',
}; };
const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />))); const { container } = render(routerWrapper(reduxWrapper(<UsernameField {...props} />)));
let closeButton = container.querySelector('button.username-suggestions__close__button'); let closeButton = container.querySelector('button.username-suggestions__close__button');
fireEvent.click(closeButton); fireEvent.click(closeButton);
expect(store.dispatch).toHaveBeenCalledWith(clearUsernameSuggestions()); expect(store.dispatch).toHaveBeenCalledWith(clearUsernameSuggestions());
@@ -287,7 +285,7 @@ describe('UsernameField', () => {
errorMessage: 'username error', errorMessage: 'username error',
}; };
render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />))); render(routerWrapper(reduxWrapper(<UsernameField {...props} />)));
closeButton = container.querySelector('button.username-suggestions__close__button'); closeButton = container.querySelector('button.username-suggestions__close__button');
fireEvent.click(closeButton); fireEvent.click(closeButton);
expect(store.dispatch).toHaveBeenCalledWith(clearUsernameSuggestions()); expect(store.dispatch).toHaveBeenCalledWith(clearUsernameSuggestions());
@@ -309,7 +307,7 @@ describe('UsernameField', () => {
store.dispatch = jest.fn(store.dispatch); store.dispatch = jest.fn(store.dispatch);
const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />))); const { container } = render(routerWrapper(reduxWrapper(<UsernameField {...props} />)));
const usernameField = container.querySelector('input#username'); const usernameField = container.querySelector('input#username');
fireEvent.focus(usernameField, { target: { value: 'test', name: 'username' } }); fireEvent.focus(usernameField, { target: { value: 'test', name: 'username' } });

View File

@@ -4,7 +4,6 @@ import React, {
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { getConfig } from '@edx/frontend-platform'; import { getConfig } from '@edx/frontend-platform';
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
import { useIntl } from '@edx/frontend-platform/i18n'; import { useIntl } from '@edx/frontend-platform/i18n';
import { Form, Spinner, StatefulButton } from '@openedx/paragon'; import { Form, Spinner, StatefulButton } from '@openedx/paragon';
import classNames from 'classnames'; import classNames from 'classnames';
@@ -12,12 +11,19 @@ import PropTypes from 'prop-types';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import Skeleton from 'react-loading-skeleton'; import Skeleton from 'react-loading-skeleton';
import {
InstitutionLogistration,
PasswordField,
RedirectLogistration,
ThirdPartyAuthAlert,
} from '../common-components';
import ConfigurableRegistrationForm from './components/ConfigurableRegistrationForm'; import ConfigurableRegistrationForm from './components/ConfigurableRegistrationForm';
import RegistrationFailure from './components/RegistrationFailure'; import RegistrationFailure from './components/RegistrationFailure';
import { import {
backupRegistrationFormBegin, backupRegistrationFormBegin,
clearRegistrationBackendError, clearRegistrationBackendError,
registerNewUser, registerNewUser,
setAutoGeneratedUsernameExperimentData,
setEmailSuggestionInStore, setEmailSuggestionInStore,
setUserPipelineDataLoaded, setUserPipelineDataLoaded,
} from './data/actions'; } from './data/actions';
@@ -25,6 +31,9 @@ import {
FORM_SUBMISSION_ERROR, FORM_SUBMISSION_ERROR,
TPA_AUTHENTICATION_FAILURE, TPA_AUTHENTICATION_FAILURE,
} from './data/constants'; } from './data/constants';
import useRecaptchaSubmission from './data/hooks';
import { AUTO_GENERATED_USERNAME_REGISTRATION_EXP_VARIATION, NOT_INITIALIZED } from './data/optimizelyExperiment/helper';
import useAutoGeneratedUsernameExperimentVariation from './data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation';
import getBackendValidations from './data/selectors'; import getBackendValidations from './data/selectors';
import { import {
isFormValid, prepareRegistrationPayload, isFormValid, prepareRegistrationPayload,
@@ -32,21 +41,20 @@ import {
import messages from './messages'; import messages from './messages';
import { EmailField, NameField, UsernameField } from './RegistrationFields'; import { EmailField, NameField, UsernameField } from './RegistrationFields';
import { import {
InstitutionLogistration, ELEMENT_NAME, ELEMENT_TEXT, ELEMENT_TYPES, PAGE_TYPES,
PasswordField, } from '../cohesion/constants';
RedirectLogistration, import { setCohesionEventStates } from '../cohesion/data/actions';
ThirdPartyAuthAlert,
} from '../common-components';
import { getThirdPartyAuthContext as getRegistrationDataFromBackend } from '../common-components/data/actions'; import { getThirdPartyAuthContext as getRegistrationDataFromBackend } from '../common-components/data/actions';
import EnterpriseSSO from '../common-components/EnterpriseSSO'; import EnterpriseSSO from '../common-components/EnterpriseSSO';
import ThirdPartyAuth from '../common-components/ThirdPartyAuth'; import ThirdPartyAuth from '../common-components/ThirdPartyAuth';
import { import {
COMPLETE_STATE, PENDING_STATE, REGISTER_PAGE, APP_NAME, COMPLETE_STATE, PENDING_STATE,
REGISTER_PAGE,
} from '../data/constants'; } from '../data/constants';
import { import {
getAllPossibleQueryParams, getTpaHint, getTpaProvider, isHostAvailableInQueryParams, setCookie, getAllPossibleQueryParams, getTpaHint, getTpaProvider, isHostAvailableInQueryParams, removeCookie, setCookie,
} from '../data/utils'; } from '../data/utils';
import { trackRegistrationPageViewed, trackRegistrationSuccess } from '../tracking/trackers/register';
/** /**
* Main Registration Page component * Main Registration Page component
*/ */
@@ -54,12 +62,14 @@ const RegistrationPage = (props) => {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const dispatch = useDispatch(); const dispatch = useDispatch();
const { executeWithFallback } = useRecaptchaSubmission('submit_registration_form');
const registrationEmbedded = isHostAvailableInQueryParams(); const registrationEmbedded = isHostAvailableInQueryParams();
const platformName = getConfig().SITE_NAME; const platformName = getConfig().SITE_NAME;
const flags = { const flags = {
showConfigurableEdxFields: getConfig().SHOW_CONFIGURABLE_EDX_FIELDS, showConfigurableEdxFields: getConfig().SHOW_CONFIGURABLE_EDX_FIELDS,
showConfigurableRegistrationFields: getConfig().ENABLE_DYNAMIC_REGISTRATION_FIELDS, showConfigurableRegistrationFields: getConfig().ENABLE_DYNAMIC_REGISTRATION_FIELDS,
showMarketingEmailOptInCheckbox: getConfig().MARKETING_EMAILS_OPT_IN, showMarketingEmailOptInCheckbox: getConfig().MARKETING_EMAILS_OPT_IN,
autoGeneratedUsernameEnabled: getConfig().ENABLE_AUTO_GENERATED_USERNAME,
}; };
const { const {
handleInstitutionLogin, handleInstitutionLogin,
@@ -67,6 +77,7 @@ const RegistrationPage = (props) => {
} = props; } = props;
const backedUpFormData = useSelector(state => state.register.registrationFormData); const backedUpFormData = useSelector(state => state.register.registrationFormData);
const initExpVariation = useSelector(state => state.register.autoGeneratedUsernameExperimentVariation);
const registrationError = useSelector(state => state.register.registrationError); const registrationError = useSelector(state => state.register.registrationError);
const registrationErrorCode = registrationError?.errorCode; const registrationErrorCode = registrationError?.errorCode;
const registrationResult = useSelector(state => state.register.registrationResult); const registrationResult = useSelector(state => state.register.registrationResult);
@@ -84,6 +95,7 @@ const RegistrationPage = (props) => {
const providers = useSelector(state => state.commonComponents.thirdPartyAuthContext.providers); const providers = useSelector(state => state.commonComponents.thirdPartyAuthContext.providers);
const secondaryProviders = useSelector(state => state.commonComponents.thirdPartyAuthContext.secondaryProviders); const secondaryProviders = useSelector(state => state.commonComponents.thirdPartyAuthContext.secondaryProviders);
const pipelineUserDetails = useSelector(state => state.commonComponents.thirdPartyAuthContext.pipelineUserDetails); const pipelineUserDetails = useSelector(state => state.commonComponents.thirdPartyAuthContext.pipelineUserDetails);
const countriesCodesList = useSelector(state => state.commonComponents.countriesCodesList);
const backendValidations = useSelector(getBackendValidations); const backendValidations = useSelector(getBackendValidations);
const queryParams = useMemo(() => getAllPossibleQueryParams(), []); const queryParams = useMemo(() => getAllPossibleQueryParams(), []);
@@ -102,6 +114,12 @@ const RegistrationPage = (props) => {
? formatMessage(messages['create.account.cta.button'], { label: cta }) ? formatMessage(messages['create.account.cta.button'], { label: cta })
: formatMessage(messages['create.account.for.free.button']); : formatMessage(messages['create.account.for.free.button']);
const autoGeneratedUsernameExpVariation = useAutoGeneratedUsernameExperimentVariation(
initExpVariation, registrationEmbedded, tpaHint, currentProvider, thirdPartyAuthApiStatus,
);
const hideUsernameField = flags.autoGeneratedUsernameEnabled
|| autoGeneratedUsernameExpVariation === AUTO_GENERATED_USERNAME_REGISTRATION_EXP_VARIATION;
/** /**
* Set the userPipelineDetails data in formFields for only first time * Set the userPipelineDetails data in formFields for only first time
*/ */
@@ -127,7 +145,7 @@ const RegistrationPage = (props) => {
useEffect(() => { useEffect(() => {
if (!formStartTime) { if (!formStartTime) {
sendPageEvent('login_and_registration', 'register'); trackRegistrationPageViewed();
const payload = { ...queryParams, is_register_page: true }; const payload = { ...queryParams, is_register_page: true };
if (tpaHint) { if (tpaHint) {
payload.tpa_hint = tpaHint; payload.tpa_hint = tpaHint;
@@ -148,8 +166,10 @@ const RegistrationPage = (props) => {
formFields: { ...formFields }, formFields: { ...formFields },
errors: { ...errors }, errors: { ...errors },
})); }));
dispatch(setAutoGeneratedUsernameExperimentData(autoGeneratedUsernameExpVariation));
} }
}, [shouldBackupState, configurableFormFields, formFields, errors, dispatch, backedUpFormData]); }, [shouldBackupState, configurableFormFields, // eslint-disable-line react-hooks/exhaustive-deps
formFields, errors, dispatch, backedUpFormData]);
useEffect(() => { useEffect(() => {
if (backendValidations) { if (backendValidations) {
@@ -170,10 +190,15 @@ const RegistrationPage = (props) => {
useEffect(() => { useEffect(() => {
if (registrationResult.success) { if (registrationResult.success) {
// This event is used by GTM // This event is used by GTM
sendTrackEvent('edx.bi.user.account.registered.client', {}); trackRegistrationSuccess();
// This is used by the "User Retention Rate Event" on GTM // This is used by the "User Retention Rate Event" on GTM
setCookie(getConfig().USER_RETENTION_COOKIE_NAME, true); setCookie(getConfig().USER_RETENTION_COOKIE_NAME, true);
// Remove marketingEmailsOptIn cookie that was set on SSO registration flow
removeCookie('marketingEmailsOptIn');
// Remove this cookie that was set to capture marketingEmailsOptIn for the onboarding component
removeCookie('ssoPipelineRedirectionDone');
} }
}, [registrationResult]); }, [registrationResult]);
@@ -207,14 +232,17 @@ const RegistrationPage = (props) => {
} }
}; };
const registerUser = () => { const registerUser = async () => {
const totalRegistrationTime = (Date.now() - formStartTime) / 1000; const totalRegistrationTime = (Date.now() - formStartTime) / 1000;
let payload = { ...formFields }; let payload = { ...formFields, app_name: APP_NAME };
if (currentProvider) { if (currentProvider) {
delete payload.password; delete payload.password;
payload.social_auth_provider = currentProvider; payload.social_auth_provider = currentProvider;
} }
if (hideUsernameField) {
delete payload.username;
}
// Validating form data before submitting // Validating form data before submitting
const { isValid, fieldErrors, emailSuggestion } = isFormValid( const { isValid, fieldErrors, emailSuggestion } = isFormValid(
@@ -224,7 +252,7 @@ const RegistrationPage = (props) => {
fieldDescriptions, fieldDescriptions,
formatMessage, formatMessage,
); );
setErrors({ ...fieldErrors }); setErrors({ ...fieldErrors, captchaError: '' });
dispatch(setEmailSuggestionInStore(emailSuggestion)); dispatch(setEmailSuggestionInStore(emailSuggestion));
// returning if not valid // returning if not valid
@@ -233,20 +261,41 @@ const RegistrationPage = (props) => {
return; return;
} }
// Preparing payload for submission let recaptchaToken = null;
try {
recaptchaToken = await executeWithFallback();
} catch (err) {
setErrors(prev => ({
...prev,
captchaError: err.message,
}));
return;
}
payload = prepareRegistrationPayload( payload = prepareRegistrationPayload(
payload, payload,
configurableFormFields, configurableFormFields,
flags.showMarketingEmailOptInCheckbox, flags.showMarketingEmailOptInCheckbox,
totalRegistrationTime, totalRegistrationTime,
queryParams); queryParams,
);
if (recaptchaToken) {
payload = { ...payload, captcha_token: recaptchaToken };
}
// making register call
dispatch(registerNewUser(payload)); dispatch(registerNewUser(payload));
}; };
const handleSubmit = (e) => { const handleSubmit = (e) => {
e.preventDefault(); e.preventDefault();
const eventData = {
pageType: PAGE_TYPES.ACCOUNT_CREATION,
elementType: ELEMENT_TYPES.BUTTON,
webElementText: ELEMENT_TEXT.CREATE_ACCOUNT,
webElementName: ELEMENT_NAME.CREATE_ACCOUNT,
};
dispatch(setCohesionEventStates(eventData));
registerUser(); registerUser();
}; };
@@ -281,105 +330,117 @@ const RegistrationPage = (props) => {
redirectToProgressiveProfilingPage={ redirectToProgressiveProfilingPage={
getConfig().ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN && !!Object.keys(optionalFields.fields).length getConfig().ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN && !!Object.keys(optionalFields.fields).length
} }
currentProvider={currentProvider}
/> />
{autoSubmitRegForm && !errorCode.type ? ( {(autoSubmitRegForm && !errorCode.type)
<div className="mw-xs mt-5 text-center"> || (!autoGeneratedUsernameExpVariation && !(
<Spinner animation="border" variant="primary" id="tpa-spinner" /> autoGeneratedUsernameExpVariation === NOT_INITIALIZED
</div> || registrationEmbedded || !!tpaHint || !!currentProvider))
) : ( ? (
<div <div className="mw-xs mt-5 text-center">
className={classNames( <Spinner animation="border" variant="primary" id="tpa-spinner" />
'mw-xs mt-3', </div>
{ 'w-100 m-auto pt-4 main-content': registrationEmbedded }, ) : (
)} <div
> className={classNames(
<ThirdPartyAuthAlert 'mw-xs mt-3',
currentProvider={currentProvider} { 'w-100 m-auto pt-4 main-content': registrationEmbedded },
platformName={platformName} )}
referrer={REGISTER_PAGE} >
/> <ThirdPartyAuthAlert
<RegistrationFailure currentProvider={currentProvider}
errorCode={errorCode.type} platformName={platformName}
failureCount={errorCode.count} referrer={REGISTER_PAGE}
context={{ provider: currentProvider, errorMessage: thirdPartyAuthErrorMessage }}
/>
<Form id="registration-form" name="registration-form">
<NameField
name="name"
value={formFields.name}
shouldFetchUsernameSuggestions={!formFields.username.trim()}
handleChange={handleOnChange}
handleErrorChange={handleErrorChange}
errorMessage={errors.name}
helpText={[formatMessage(messages['help.text.name'])]}
floatingLabel={formatMessage(messages['registration.fullname.label'])}
/> />
<EmailField <RegistrationFailure
name="email" errorCode={errorCode.type}
value={formFields.email} failureCount={errorCode.count}
confirmEmailValue={configurableFormFields?.confirm_email} context={{ provider: currentProvider, errorMessage: thirdPartyAuthErrorMessage }}
handleErrorChange={handleErrorChange}
handleChange={handleOnChange}
errorMessage={errors.email}
helpText={[formatMessage(messages['help.text.email'])]}
floatingLabel={formatMessage(messages['registration.email.label'])}
/> />
<UsernameField <Form id="registration-form" name="registration-form">
name="username" <NameField
spellCheck="false" name="name"
value={formFields.username} value={formFields.name}
handleChange={handleOnChange} shouldFetchUsernameSuggestions={!formFields.username.trim()}
handleErrorChange={handleErrorChange}
errorMessage={errors.username}
helpText={[formatMessage(messages['help.text.username.1']), formatMessage(messages['help.text.username.2'])]}
floatingLabel={formatMessage(messages['registration.username.label'])}
/>
{!currentProvider && (
<PasswordField
name="password"
value={formFields.password}
handleChange={handleOnChange} handleChange={handleOnChange}
handleErrorChange={handleErrorChange} handleErrorChange={handleErrorChange}
errorMessage={errors.password} errorMessage={errors.name}
floatingLabel={formatMessage(messages['registration.password.label'])} helpText={[formatMessage(messages['help.text.name'])]}
floatingLabel={formatMessage(messages['registration.fullname.label'])}
/> />
)} <EmailField
<ConfigurableRegistrationForm name="email"
email={formFields.email} value={formFields.email}
fieldErrors={errors} confirmEmailValue={configurableFormFields?.confirm_email}
formFields={configurableFormFields} handleErrorChange={handleErrorChange}
setFieldErrors={registrationEmbedded ? setTemporaryErrors : setErrors} handleChange={handleOnChange}
setFormFields={setConfigurableFormFields} errorMessage={errors.email}
autoSubmitRegisterForm={autoSubmitRegForm} helpText={[formatMessage(messages['help.text.email'])]}
fieldDescriptions={fieldDescriptions} floatingLabel={formatMessage(messages['registration.email.label'])}
/>
<StatefulButton
id="register-user"
name="register-user"
type="submit"
variant="brand"
className="register-button mt-4 mb-4"
state={submitState}
labels={{
default: buttonLabel,
pending: '',
}}
onClick={handleSubmit}
onMouseDown={(e) => e.preventDefault()}
/>
{!registrationEmbedded && (
<ThirdPartyAuth
currentProvider={currentProvider}
providers={providers}
secondaryProviders={secondaryProviders}
handleInstitutionLogin={handleInstitutionLogin}
thirdPartyAuthApiStatus={thirdPartyAuthApiStatus}
/> />
)} {!hideUsernameField && (
</Form> <UsernameField
</div> name="username"
)} spellCheck="false"
value={formFields.username}
handleChange={handleOnChange}
handleErrorChange={handleErrorChange}
errorMessage={errors.username}
helpText={[formatMessage(messages['help.text.username.1']), formatMessage(messages['help.text.username.2'])]}
floatingLabel={formatMessage(messages['registration.username.label'])}
/>
)}
{!currentProvider && (
<PasswordField
name="password"
value={formFields.password}
handleChange={handleOnChange}
handleErrorChange={handleErrorChange}
errorMessage={errors.password}
floatingLabel={formatMessage(messages['registration.password.label'])}
/>
)}
<ConfigurableRegistrationForm
email={formFields.email}
fieldErrors={errors}
formFields={configurableFormFields}
setFieldErrors={registrationEmbedded ? setTemporaryErrors : setErrors}
setFormFields={setConfigurableFormFields}
autoSubmitRegisterForm={autoSubmitRegForm}
fieldDescriptions={fieldDescriptions}
countriesCodesList={countriesCodesList}
/>
{errors?.captchaError && (
<div className="mt-3 pgn__form-text-invalid pgn__form-text">
{errors.captchaError}
</div>
)}
<StatefulButton
id="register-user"
name="register-user"
type="submit"
variant="brand"
className="register-button mt-4 mb-4"
state={submitState}
labels={{
default: buttonLabel,
pending: '',
}}
onClick={handleSubmit}
onMouseDown={(e) => e.preventDefault()}
/>
{!registrationEmbedded && (
<ThirdPartyAuth
currentProvider={currentProvider}
providers={providers}
secondaryProviders={secondaryProviders}
handleInstitutionLogin={handleInstitutionLogin}
thirdPartyAuthApiStatus={thirdPartyAuthApiStatus}
/>
)}
</Form>
</div>
)}
</> </>
); );
}; };

View File

@@ -1,12 +1,11 @@
import React from 'react';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { getConfig, mergeConfig } from '@edx/frontend-platform'; import { getConfig, mergeConfig } from '@edx/frontend-platform';
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics'; import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
import { import {
configure, getLocale, injectIntl, IntlProvider, configure, getLocale, IntlProvider,
} from '@edx/frontend-platform/i18n'; } from '@edx/frontend-platform/i18n';
import { fireEvent, render } from '@testing-library/react'; import { fireEvent, render, waitFor } from '@testing-library/react';
import { mockNavigate, BrowserRouter as Router } from 'react-router-dom'; import { mockNavigate, BrowserRouter as Router } from 'react-router-dom';
import configureStore from 'redux-mock-store'; import configureStore from 'redux-mock-store';
@@ -17,9 +16,14 @@ import {
setUserPipelineDataLoaded, setUserPipelineDataLoaded,
} from './data/actions'; } from './data/actions';
import { INTERNAL_SERVER_ERROR } from './data/constants'; import { INTERNAL_SERVER_ERROR } from './data/constants';
import mockTagular from '../cohesion/utils';
import useRecaptchaSubmission from './data/hooks';
import { NOT_INITIALIZED } from './data/optimizelyExperiment/helper';
import useAutoGeneratedUsernameExperimentVariation
from './data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation';
import RegistrationPage from './RegistrationPage'; import RegistrationPage from './RegistrationPage';
import { import {
AUTHN_PROGRESSIVE_PROFILING, COMPLETE_STATE, PENDING_STATE, REGISTER_PAGE, APP_NAME, AUTHN_PROGRESSIVE_PROFILING, COMPLETE_STATE, PENDING_STATE, REGISTER_PAGE,
} from '../data/constants'; } from '../data/constants';
jest.mock('@edx/frontend-platform/analytics', () => ({ jest.mock('@edx/frontend-platform/analytics', () => ({
@@ -30,9 +34,16 @@ jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'), ...jest.requireActual('@edx/frontend-platform/i18n'),
getLocale: jest.fn(), getLocale: jest.fn(),
})); }));
jest.mock('./data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation', () => jest.fn());
jest.mock('./data/hooks', () => ({
__esModule: true,
default: jest.fn(() => ({
executeWithFallback: jest.fn(),
})),
}));
const IntlRegistrationPage = injectIntl(RegistrationPage);
const mockStore = configureStore(); const mockStore = configureStore();
mockTagular();
jest.mock('react-router-dom', () => { jest.mock('react-router-dom', () => {
const mockNavigation = jest.fn(); const mockNavigation = jest.fn();
@@ -102,6 +113,7 @@ describe('RegistrationPage', () => {
usernameSuggestions: [], usernameSuggestions: [],
}, },
cohesion: { eventData: {} },
commonComponents: { commonComponents: {
thirdPartyAuthApiStatus: null, thirdPartyAuthApiStatus: null,
thirdPartyAuthContext, thirdPartyAuthContext,
@@ -128,15 +140,28 @@ describe('RegistrationPage', () => {
institutionLogin: false, institutionLogin: false,
}; };
window.location = { search: '' }; window.location = { search: '' };
useAutoGeneratedUsernameExperimentVariation.mockReturnValue(NOT_INITIALIZED);
useRecaptchaSubmission.mockReturnValue({
executeWithFallback: jest.fn().mockResolvedValue(null),
isReady: true,
isLoading: false,
});
}); });
afterEach(() => { afterEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();
}); });
const populateRequiredFields = (getByLabelText, payload, isThirdPartyAuth = false) => { const populateRequiredFields = (
getByLabelText,
payload,
isThirdPartyAuth = false,
autoGeneratedUsernameEnabled = false,
) => {
fireEvent.change(getByLabelText('Full name'), { target: { value: payload.name, name: 'name' } }); fireEvent.change(getByLabelText('Full name'), { target: { value: payload.name, name: 'name' } });
fireEvent.change(getByLabelText('Public username'), { target: { value: payload.username, name: 'username' } }); if (!autoGeneratedUsernameEnabled) {
fireEvent.change(getByLabelText('Public username'), { target: { value: payload.username, name: 'username' } });
}
fireEvent.change(getByLabelText('Email'), { target: { value: payload.email, name: 'email' } }); fireEvent.change(getByLabelText('Email'), { target: { value: payload.email, name: 'email' } });
fireEvent.change(getByLabelText('Country/Region'), { target: { value: payload.country, name: 'country' } }); fireEvent.change(getByLabelText('Country/Region'), { target: { value: payload.country, name: 'country' } });
@@ -162,7 +187,7 @@ describe('RegistrationPage', () => {
// ******** test registration form submission ******** // ******** test registration form submission ********
it('should submit form for valid input', () => { it('should submit form for valid input', async () => {
getLocale.mockImplementation(() => ('en-us')); getLocale.mockImplementation(() => ('en-us'));
jest.spyOn(global.Date, 'now').mockImplementation(() => 0); jest.spyOn(global.Date, 'now').mockImplementation(() => 0);
@@ -176,20 +201,23 @@ describe('RegistrationPage', () => {
password: 'password1', password: 'password1',
country: 'Pakistan', country: 'Pakistan',
honor_code: true, honor_code: true,
totalRegistrationTime: 0, total_registration_time: 0,
next: '/course/demo-course-url', next: '/course/demo-course-url',
app_name: APP_NAME,
}; };
store.dispatch = jest.fn(store.dispatch); store.dispatch = jest.fn(store.dispatch);
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />))); const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
populateRequiredFields(getByLabelText, payload); populateRequiredFields(getByLabelText, payload);
const button = container.querySelector('button.btn-brand'); const button = container.querySelector('button.btn-brand');
fireEvent.click(button); fireEvent.click(button);
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK' })); await waitFor(() => {
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK' }));
});
}); });
it('should submit form without password field when current provider is present', () => { it('should submit form without password field when current provider is present', async () => {
jest.spyOn(global.Date, 'now').mockImplementation(() => 0); jest.spyOn(global.Date, 'now').mockImplementation(() => 0);
const formPayload = { const formPayload = {
@@ -199,7 +227,8 @@ describe('RegistrationPage', () => {
country: 'Pakistan', country: 'Pakistan',
honor_code: true, honor_code: true,
social_auth_provider: 'Apple', social_auth_provider: 'Apple',
totalRegistrationTime: 0, total_registration_time: 0,
app_name: APP_NAME,
}; };
store = mockStore({ store = mockStore({
@@ -213,12 +242,14 @@ describe('RegistrationPage', () => {
}, },
}); });
store.dispatch = jest.fn(store.dispatch); store.dispatch = jest.fn(store.dispatch);
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />))); const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
populateRequiredFields(getByLabelText, formPayload, true); populateRequiredFields(getByLabelText, formPayload, true);
const button = container.querySelector('button.btn-brand'); const button = container.querySelector('button.btn-brand');
fireEvent.click(button); fireEvent.click(button);
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...formPayload, country: 'PK' })); await waitFor(() => {
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...formPayload, country: 'PK' }));
});
}); });
it('should display an error when form is submitted with an invalid email', () => { it('should display an error when form is submitted with an invalid email', () => {
@@ -232,11 +263,11 @@ describe('RegistrationPage', () => {
password: 'password1', password: 'password1',
country: 'Ukraine', country: 'Ukraine',
honor_code: true, honor_code: true,
totalRegistrationTime: 0, total_registration_time: 0,
}; };
store.dispatch = jest.fn(store.dispatch); store.dispatch = jest.fn(store.dispatch);
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />))); const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
populateRequiredFields(getByLabelText, formPayload, true); populateRequiredFields(getByLabelText, formPayload, true);
const button = container.querySelector('button.btn-brand'); const button = container.querySelector('button.btn-brand');
@@ -257,11 +288,11 @@ describe('RegistrationPage', () => {
password: 'password1', password: 'password1',
country: 'Ukraine', country: 'Ukraine',
honor_code: true, honor_code: true,
totalRegistrationTime: 0, total_registration_time: 0,
}; };
store.dispatch = jest.fn(store.dispatch); store.dispatch = jest.fn(store.dispatch);
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />))); const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
populateRequiredFields(getByLabelText, formPayload, true); populateRequiredFields(getByLabelText, formPayload, true);
const button = container.querySelector('button.btn-brand'); const button = container.querySelector('button.btn-brand');
fireEvent.click(button); fireEvent.click(button);
@@ -269,7 +300,7 @@ describe('RegistrationPage', () => {
expect(validationErrors.textContent).toContain(usernameError); expect(validationErrors.textContent).toContain(usernameError);
}); });
it('should submit form with marketing email opt in value', () => { it('should submit form with marketing email opt in value', async () => {
mergeConfig({ mergeConfig({
MARKETING_EMAILS_OPT_IN: 'true', MARKETING_EMAILS_OPT_IN: 'true',
}); });
@@ -283,26 +314,70 @@ describe('RegistrationPage', () => {
password: 'password1', password: 'password1',
country: 'Pakistan', country: 'Pakistan',
honor_code: true, honor_code: true,
totalRegistrationTime: 0, total_registration_time: 0,
marketing_emails_opt_in: true, marketing_emails_opt_in: true,
app_name: APP_NAME,
}; };
store.dispatch = jest.fn(store.dispatch); store.dispatch = jest.fn(store.dispatch);
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />))); const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
populateRequiredFields(getByLabelText, payload); populateRequiredFields(getByLabelText, payload);
const button = container.querySelector('button.btn-brand'); const button = container.querySelector('button.btn-brand');
fireEvent.click(button); fireEvent.click(button);
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK' })); await waitFor(() => {
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK' }));
});
mergeConfig({ mergeConfig({
MARKETING_EMAILS_OPT_IN: '', MARKETING_EMAILS_OPT_IN: '',
}); });
}); });
it('should submit form without UsernameField when autoGeneratedUsernameEnabled is true', async () => {
mergeConfig({
ENABLE_AUTO_GENERATED_USERNAME: true,
});
jest.spyOn(global.Date, 'now').mockImplementation(() => 0);
const payload = {
name: 'John Doe',
email: 'john.doe@gmail.com',
password: 'password1',
country: 'Pakistan',
honor_code: true,
total_registration_time: 0,
app_name: APP_NAME,
};
store.dispatch = jest.fn(store.dispatch);
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
populateRequiredFields(getByLabelText, payload, false, true);
const button = container.querySelector('button.btn-brand');
fireEvent.click(button);
await waitFor(() => {
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK' }));
});
mergeConfig({
ENABLE_AUTO_GENERATED_USERNAME: false,
});
});
it('should not display UsernameField when ENABLE_AUTO_GENERATED_USERNAME is true', () => {
mergeConfig({
ENABLE_AUTO_GENERATED_USERNAME: true,
});
const { queryByLabelText } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
expect(queryByLabelText('Username')).toBeNull();
mergeConfig({
ENABLE_AUTO_GENERATED_USERNAME: false,
});
});
it('should not dispatch registerNewUser on empty form Submission', () => { it('should not dispatch registerNewUser on empty form Submission', () => {
store.dispatch = jest.fn(store.dispatch); store.dispatch = jest.fn(store.dispatch);
const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />))); const { container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
const button = container.querySelector('button.btn-brand'); const button = container.querySelector('button.btn-brand');
fireEvent.click(button); fireEvent.click(button);
@@ -312,8 +387,126 @@ describe('RegistrationPage', () => {
// ******** test registration form validations ******** // ******** test registration form validations ********
it('should submit form with valid reCAPTCHA token', async () => {
getLocale.mockImplementation(() => ('en-us'));
jest.spyOn(global.Date, 'now').mockImplementation(() => 0);
useRecaptchaSubmission.mockReturnValue({
executeWithFallback: jest.fn().mockResolvedValue('mock-recaptcha-token'),
isReady: true,
isLoading: false,
});
const payload = {
name: 'John Doe',
username: 'john_doe',
email: 'john.doe@gmail.com',
password: 'password1',
country: 'Pakistan',
honor_code: true,
total_registration_time: 0,
app_name: APP_NAME,
};
store.dispatch = jest.fn(store.dispatch);
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
populateRequiredFields(getByLabelText, payload);
const button = container.querySelector('button.btn-brand');
fireEvent.click(button);
await waitFor(() => {
const actions = store.dispatch.mock.calls.map(call => call[0]);
const registerAction = actions.find(a => a.type === registerNewUser().type);
expect(registerAction).toBeTruthy();
expect(registerAction.payload).toMatchObject({
registrationInfo: {
...payload,
country: 'PK',
captcha_token: 'mock-recaptcha-token',
},
});
});
});
it('should display error when reCAPTCHA verification fails', async () => {
getLocale.mockImplementation(() => ('en-us'));
jest.spyOn(global.Date, 'now').mockImplementation(() => 0);
useRecaptchaSubmission.mockReturnValue({
executeWithFallback: jest.fn().mockRejectedValue(new Error('CAPTCHA verification failed.')),
isReady: true,
isLoading: false,
});
const payload = {
name: 'John Doe',
username: 'john_doe',
email: 'john.doe@gmail.com',
password: 'password1',
country: 'Pakistan',
honor_code: true,
total_registration_time: 0,
};
store.dispatch = jest.fn(store.dispatch);
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
populateRequiredFields(getByLabelText, payload);
const button = container.querySelector('button.btn-brand');
fireEvent.click(button);
await waitFor(() => {
const captchaError = container.querySelector('.pgn__form-text-invalid');
expect(captchaError.textContent).toContain('CAPTCHA verification failed.');
});
expect(store.dispatch).not.toHaveBeenCalledWith(expect.objectContaining({
type: registerNewUser().type,
}));
});
it('should submit without reCAPTCHA token if reCAPTCHA is disabled', async () => {
getLocale.mockImplementation(() => ('en-us'));
jest.spyOn(global.Date, 'now').mockImplementation(() => 0);
useRecaptchaSubmission.mockReturnValue({
executeWithFallback: jest.fn().mockResolvedValue(null),
isReady: true,
isLoading: false,
});
const payload = {
name: 'John Doe',
username: 'john_doe',
email: 'john.doe@gmail.com',
password: 'password1',
country: 'Pakistan',
honor_code: true,
total_registration_time: 0,
app_name: APP_NAME,
};
store.dispatch = jest.fn(store.dispatch);
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
populateRequiredFields(getByLabelText, payload);
const button = container.querySelector('button.btn-brand');
fireEvent.click(button);
await waitFor(() => {
expect(store.dispatch).toHaveBeenCalledWith(
registerNewUser({
...payload,
country: 'PK',
}),
);
});
});
it('should show error messages for required fields on empty form submission', () => { it('should show error messages for required fields on empty form submission', () => {
const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />))); const { container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
const button = container.querySelector('button.btn-brand'); const button = container.querySelector('button.btn-brand');
fireEvent.click(button); fireEvent.click(button);
@@ -341,7 +534,7 @@ describe('RegistrationPage', () => {
}, },
}, },
}); });
const { container } = render(routerWrapper(reduxWrapper(<IntlProvider locale="en"><IntlRegistrationPage {...props} /></IntlProvider>))); const { container } = render(routerWrapper(reduxWrapper(<IntlProvider locale="en"><RegistrationPage {...props} /></IntlProvider>)));
const usernameFeedback = container.querySelector('div[feedback-for="username"]'); const usernameFeedback = container.querySelector('div[feedback-for="username"]');
const emailFeedback = container.querySelector('div[feedback-for="email"]'); const emailFeedback = container.querySelector('div[feedback-for="email"]');
@@ -350,7 +543,7 @@ describe('RegistrationPage', () => {
}); });
it('should clear error on focus', () => { it('should clear error on focus', () => {
const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />))); const { container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
const submitButton = container.querySelector('button.btn-brand'); const submitButton = container.querySelector('button.btn-brand');
fireEvent.click(submitButton); fireEvent.click(submitButton);
@@ -379,7 +572,7 @@ describe('RegistrationPage', () => {
store.dispatch = jest.fn(store.dispatch); store.dispatch = jest.fn(store.dispatch);
const { container } = render(routerWrapper(reduxWrapper( const { container } = render(routerWrapper(reduxWrapper(
<IntlRegistrationPage {...props} />, <RegistrationPage {...props} />,
))); )));
const emailInput = container.querySelector('input#email'); const emailInput = container.querySelector('input#email');
@@ -390,7 +583,7 @@ describe('RegistrationPage', () => {
// ******** test form buttons and fields ******** // ******** test form buttons and fields ********
it('should match default button state', () => { it('should match default button state', () => {
const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />))); const { container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
const button = container.querySelector('button[type="submit"] span'); const button = container.querySelector('button[type="submit"] span');
expect(button.textContent).toEqual('Create an account for free'); expect(button.textContent).toEqual('Create an account for free');
}); });
@@ -404,7 +597,7 @@ describe('RegistrationPage', () => {
}, },
}); });
const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />))); const { container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
const button = container.querySelector('button[type="submit"] span.sr-only'); const button = container.querySelector('button[type="submit"] span.sr-only');
expect(button.textContent).toEqual('pending'); expect(button.textContent).toEqual('pending');
@@ -415,7 +608,7 @@ describe('RegistrationPage', () => {
MARKETING_EMAILS_OPT_IN: 'true', MARKETING_EMAILS_OPT_IN: 'true',
}); });
const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />))); const { container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
const checkboxDivs = container.querySelectorAll('div.form-field--checkbox'); const checkboxDivs = container.querySelectorAll('div.form-field--checkbox');
expect(checkboxDivs.length).toEqual(1); expect(checkboxDivs.length).toEqual(1);
@@ -428,7 +621,7 @@ describe('RegistrationPage', () => {
const buttonLabel = 'Register'; const buttonLabel = 'Register';
delete window.location; delete window.location;
window.location = { href: getConfig().BASE_URL, search: `?cta=${buttonLabel}` }; window.location = { href: getConfig().BASE_URL, search: `?cta=${buttonLabel}` };
const { container } = render(reduxWrapper(<IntlRegistrationPage {...props} />)); const { container } = render(reduxWrapper(<RegistrationPage {...props} />));
const button = container.querySelector('button[type="submit"] span'); const button = container.querySelector('button[type="submit"] span');
const buttonText = button.textContent; const buttonText = button.textContent;
@@ -447,11 +640,11 @@ describe('RegistrationPage', () => {
}, },
}); });
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />))); render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
expect(document.cookie).toMatch(`${getConfig().USER_RETENTION_COOKIE_NAME}=true`); expect(document.cookie).toMatch(`${getConfig().USER_RETENTION_COOKIE_NAME}=true`);
}); });
it('should redirect to url returned in registration result after successful account creation', () => { it('should redirect to url returned in registration result after successful account creation', async () => {
const dashboardURL = 'https://test.com/testing-dashboard/'; const dashboardURL = 'https://test.com/testing-dashboard/';
store = mockStore({ store = mockStore({
...initialState, ...initialState,
@@ -465,11 +658,13 @@ describe('RegistrationPage', () => {
}); });
delete window.location; delete window.location;
window.location = { href: getConfig().BASE_URL }; window.location = { href: getConfig().BASE_URL };
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />))); render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
expect(window.location.href).toBe(dashboardURL); await waitFor(() => {
expect(window.location.href).toBe(dashboardURL);
}, { timeout: 1100 });
}); });
it('should redirect to dashboard if features flags are configured but no optional fields are configured', () => { it('should redirect to dashboard if features flags are configured but no optional fields are configured', async () => {
mergeConfig({ mergeConfig({
ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN: true, ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN: true,
}); });
@@ -492,8 +687,10 @@ describe('RegistrationPage', () => {
}); });
delete window.location; delete window.location;
window.location = { href: getConfig().BASE_URL }; window.location = { href: getConfig().BASE_URL };
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />))); render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
expect(window.location.href).toBe(dashboardUrl); await waitFor(() => {
expect(window.location.href).toBe(dashboardUrl);
}, { timeout: 1100 });
}); });
it('should redirect to progressive profiling page if optional fields are configured', () => { it('should redirect to progressive profiling page if optional fields are configured', () => {
@@ -523,7 +720,7 @@ describe('RegistrationPage', () => {
render(reduxWrapper( render(reduxWrapper(
<Router> <Router>
<IntlRegistrationPage {...props} /> <RegistrationPage {...props} />
</Router>, </Router>,
)); ));
expect(mockNavigate).toHaveBeenCalledWith(AUTHN_PROGRESSIVE_PROFILING); expect(mockNavigate).toHaveBeenCalledWith(AUTHN_PROGRESSIVE_PROFILING);
@@ -541,13 +738,13 @@ describe('RegistrationPage', () => {
}); });
store.dispatch = jest.fn(store.dispatch); store.dispatch = jest.fn(store.dispatch);
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />))); render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
expect(store.dispatch).toHaveBeenCalledWith(backupRegistrationFormBegin({ ...registrationFormData })); expect(store.dispatch).toHaveBeenCalledWith(backupRegistrationFormBegin({ ...registrationFormData }));
}); });
it('should send page event when register page is rendered', () => { it('should send page event when register page is rendered', () => {
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />))); render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
expect(sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'register'); expect(sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'register', { app_name: APP_NAME });
}); });
it('should send track event when user has successfully registered', () => { it('should send track event when user has successfully registered', () => {
@@ -564,8 +761,8 @@ describe('RegistrationPage', () => {
delete window.location; delete window.location;
window.location = { href: getConfig().BASE_URL }; window.location = { href: getConfig().BASE_URL };
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />))); render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.user.account.registered.client', {}); expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.user.account.registered.client', { app_name: APP_NAME });
}); });
it('should populate form with pipeline user details', () => { it('should populate form with pipeline user details', () => {
@@ -590,7 +787,7 @@ describe('RegistrationPage', () => {
store.dispatch = jest.fn(store.dispatch); store.dispatch = jest.fn(store.dispatch);
const { container } = render(reduxWrapper( const { container } = render(reduxWrapper(
<Router> <Router>
<IntlRegistrationPage {...props} /> <RegistrationPage {...props} />
</Router>, </Router>,
)); ));
@@ -613,7 +810,7 @@ describe('RegistrationPage', () => {
}, },
}); });
const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />))); const { container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
const validationErrors = container.querySelector('div#validation-errors'); const validationErrors = container.querySelector('div#validation-errors');
expect(validationErrors.textContent).toContain( expect(validationErrors.textContent).toContain(
'An error has occurred. Try refreshing the page, or check your internet connection.', 'An error has occurred. Try refreshing the page, or check your internet connection.',
@@ -640,7 +837,7 @@ describe('RegistrationPage', () => {
}, },
}); });
const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />))); const { container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
const fullNameInput = container.querySelector('input#name'); const fullNameInput = container.querySelector('input#name');
const usernameInput = container.querySelector('input#username'); const usernameInput = container.querySelector('input#username');
@@ -686,14 +883,14 @@ describe('RegistrationPage', () => {
}, },
}, },
}); });
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />))); render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
expect(window.parent.postMessage).toHaveBeenCalledTimes(2); expect(window.parent.postMessage).toHaveBeenCalledTimes(2);
}); });
it('should not display validations error on blur event when embedded variant is rendered', () => { it('should not display validations error on blur event when embedded variant is rendered', () => {
delete window.location; delete window.location;
window.location = { href: getConfig().BASE_URL.concat(REGISTER_PAGE), search: '?host=http://localhost/host-website' }; window.location = { href: getConfig().BASE_URL.concat(REGISTER_PAGE), search: '?host=http://localhost/host-website' };
const { container } = render(reduxWrapper(<IntlRegistrationPage {...props} />)); const { container } = render(reduxWrapper(<RegistrationPage {...props} />));
const usernameInput = container.querySelector('input#username'); const usernameInput = container.querySelector('input#username');
fireEvent.blur(usernameInput, { target: { value: '', name: 'username' } }); fireEvent.blur(usernameInput, { target: { value: '', name: 'username' } });
@@ -721,7 +918,7 @@ describe('RegistrationPage', () => {
}, },
}); });
const { container } = render(routerWrapper(reduxWrapper( const { container } = render(routerWrapper(reduxWrapper(
<IntlRegistrationPage {...props} />), <RegistrationPage {...props} />),
)); ));
const usernameFeedback = container.querySelector('div[feedback-for="username"]'); const usernameFeedback = container.querySelector('div[feedback-for="username"]');
@@ -738,7 +935,7 @@ describe('RegistrationPage', () => {
search: '?host=http://localhost/host-website', search: '?host=http://localhost/host-website',
}; };
const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />))); const { container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
const submitButton = container.querySelector('button.btn-brand'); const submitButton = container.querySelector('button.btn-brand');
fireEvent.click(submitButton); fireEvent.click(submitButton);
@@ -779,7 +976,7 @@ describe('RegistrationPage', () => {
}); });
store.dispatch = jest.fn(store.dispatch); store.dispatch = jest.fn(store.dispatch);
const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />))); const { container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
const spinnerElement = container.querySelector('#tpa-spinner'); const spinnerElement = container.querySelector('#tpa-spinner');
const registrationFormElement = container.querySelector('#registration-form'); const registrationFormElement = container.querySelector('#registration-form');
@@ -787,7 +984,7 @@ describe('RegistrationPage', () => {
expect(registrationFormElement).toBeFalsy(); expect(registrationFormElement).toBeFalsy();
}); });
it('should auto register if autoSubmitRegForm is true and pipeline details are loaded', () => { it('should auto register if autoSubmitRegForm is true and pipeline details are loaded', async () => {
jest.spyOn(global.Date, 'now').mockImplementation(() => 0); jest.spyOn(global.Date, 'now').mockImplementation(() => 0);
getLocale.mockImplementation(() => ('en-us')); getLocale.mockImplementation(() => ('en-us'));
@@ -830,15 +1027,18 @@ describe('RegistrationPage', () => {
}); });
store.dispatch = jest.fn(store.dispatch); store.dispatch = jest.fn(store.dispatch);
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />))); render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ await waitFor(() => {
name: 'John Doe', expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({
username: 'john_doe', name: 'John Doe',
email: 'john.doe@example.com', username: 'john_doe',
country: 'PK', email: 'john.doe@example.com',
social_auth_provider: 'Apple', country: 'PK',
totalRegistrationTime: 0, social_auth_provider: 'Apple',
})); total_registration_time: 0,
app_name: APP_NAME,
}));
});
}); });
}); });
}); });

View File

@@ -1,10 +1,16 @@
import React, { useEffect, useMemo } from 'react'; import React, { useCallback, useEffect, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { getConfig } from '@edx/frontend-platform'; import { getConfig } from '@edx/frontend-platform';
import { getCountryList, getLocale, useIntl } from '@edx/frontend-platform/i18n'; import { getCountryList, getLocale, useIntl } from '@edx/frontend-platform/i18n';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import {
ELEMENT_NAME, ELEMENT_TEXT, ELEMENT_TYPES, PAGE_TYPES,
} from '../../cohesion/constants';
import trackCohesionEvent from '../../cohesion/trackers';
import { FormFieldRenderer } from '../../field-renderer'; import { FormFieldRenderer } from '../../field-renderer';
import { backupRegistrationFormBegin } from '../data/actions';
import { FIELDS } from '../data/constants'; import { FIELDS } from '../data/constants';
import messages from '../messages'; import messages from '../messages';
import { CountryField, HonorCode, TermsOfService } from '../RegistrationFields'; import { CountryField, HonorCode, TermsOfService } from '../RegistrationFields';
@@ -31,9 +37,14 @@ const ConfigurableRegistrationForm = (props) => {
setFieldErrors, setFieldErrors,
setFormFields, setFormFields,
autoSubmitRegistrationForm, autoSubmitRegistrationForm,
countriesCodesList,
} = props; } = props;
const dispatch = useDispatch();
const countryList = useMemo(() => getCountryList(getLocale()), []); /** The reason for adding the entry 'United States' is that Chrome browser aut-fill the form with the 'Unites
States' instead of 'United States of America' which does not exist in country dropdown list and gets the user
confused and unable to create an account. So we added the United States entry in the dropdown list.
*/
let showTermsOfServiceAndHonorCode = false; let showTermsOfServiceAndHonorCode = false;
let showCountryField = false; let showCountryField = false;
@@ -46,6 +57,8 @@ const ConfigurableRegistrationForm = (props) => {
showMarketingEmailOptInCheckbox: getConfig().MARKETING_EMAILS_OPT_IN, showMarketingEmailOptInCheckbox: getConfig().MARKETING_EMAILS_OPT_IN,
}; };
const backedUpFormData = useSelector(state => state.register.registrationFormData);
/** /**
* If auto submitting register form, we will check tos and honor code fields if they exist for feature parity. * If auto submitting register form, we will check tos and honor code fields if they exist for feature parity.
*/ */
@@ -66,6 +79,16 @@ const ConfigurableRegistrationForm = (props) => {
} }
}, [autoSubmitRegistrationForm]); // eslint-disable-line react-hooks/exhaustive-deps }, [autoSubmitRegistrationForm]); // eslint-disable-line react-hooks/exhaustive-deps
const removeDisabledCountries = useCallback((countryList) => {
if (!countriesCodesList.length) {
return countryList;
}
return countryList.filter(({ code }) => countriesCodesList.find(x => x === code));
}, [countriesCodesList]);
const countryList = useMemo(() => removeDisabledCountries(
getCountryList(getLocale()).concat([{ code: 'US', name: 'United States' }])), [removeDisabledCountries]);
const handleErrorChange = (fieldName, error) => { const handleErrorChange = (fieldName, error) => {
if (fieldName) { if (fieldName) {
setFieldErrors(prevErrors => ({ setFieldErrors(prevErrors => ({
@@ -86,6 +109,25 @@ const ConfigurableRegistrationForm = (props) => {
setFieldErrors(prevErrors => ({ ...prevErrors, [name]: '' })); setFieldErrors(prevErrors => ({ ...prevErrors, [name]: '' }));
} }
} }
// setting marketingEmailsOptIn state for SSO authentication flow for register API call
if (name === 'marketingEmailsOptIn') {
if (!value) {
const cohesionEventData = {
pageType: PAGE_TYPES.ACCOUNT_CREATION,
elementType: ELEMENT_TYPES.BUTTON,
webElementText: ELEMENT_TEXT.OPT_IN_TEXT,
webElementName: ELEMENT_NAME.OPT_OUT,
};
trackCohesionEvent(cohesionEventData);
}
dispatch(backupRegistrationFormBegin({
...backedUpFormData,
configurableFormFields: {
...backedUpFormData.configurableFormFields,
[name]: value,
},
}));
}
setFormFields(prevState => ({ ...prevState, [name]: value })); setFormFields(prevState => ({ ...prevState, [name]: value }));
}; };
@@ -227,11 +269,16 @@ ConfigurableRegistrationForm.propTypes = {
setFieldErrors: PropTypes.func.isRequired, setFieldErrors: PropTypes.func.isRequired,
setFormFields: PropTypes.func.isRequired, setFormFields: PropTypes.func.isRequired,
autoSubmitRegistrationForm: PropTypes.bool, autoSubmitRegistrationForm: PropTypes.bool,
countriesCodesList: PropTypes.arrayOf(PropTypes.shape({
code: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
})),
}; };
ConfigurableRegistrationForm.defaultProps = { ConfigurableRegistrationForm.defaultProps = {
fieldDescriptions: {}, fieldDescriptions: {},
autoSubmitRegistrationForm: false, autoSubmitRegistrationForm: false,
countriesCodesList: [],
}; };
export default ConfigurableRegistrationForm; export default ConfigurableRegistrationForm;

View File

@@ -9,6 +9,7 @@ import PropTypes from 'prop-types';
import { windowScrollTo } from '../../data/utils'; import { windowScrollTo } from '../../data/utils';
import { import {
FORBIDDEN_REQUEST, FORBIDDEN_REQUEST,
FORBIDDEN_USERNAME,
INTERNAL_SERVER_ERROR, INTERNAL_SERVER_ERROR,
TPA_AUTHENTICATION_FAILURE, TPA_AUTHENTICATION_FAILURE,
TPA_SESSION_EXPIRED, TPA_SESSION_EXPIRED,
@@ -48,6 +49,9 @@ const RegistrationFailureMessage = (props) => {
case TPA_SESSION_EXPIRED: case TPA_SESSION_EXPIRED:
errorMessage = formatMessage(messages['registration.tpa.session.expired'], { provider: context.provider }); errorMessage = formatMessage(messages['registration.tpa.session.expired'], { provider: context.provider });
break; break;
case FORBIDDEN_USERNAME:
errorMessage = formatMessage(messages['registration.forbidden.username']);
break;
default: default:
errorMessage = formatMessage(messages['registration.empty.form.submission.error']); errorMessage = formatMessage(messages['registration.empty.form.submission.error']);
break; break;

View File

@@ -1,16 +1,19 @@
import React from 'react';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { mergeConfig } from '@edx/frontend-platform'; import { mergeConfig } from '@edx/frontend-platform';
import { import {
getLocale, injectIntl, IntlProvider, getLocale, IntlProvider,
} from '@edx/frontend-platform/i18n'; } from '@edx/frontend-platform/i18n';
import { fireEvent, render } from '@testing-library/react'; import { fireEvent, render, waitFor } from '@testing-library/react';
import { BrowserRouter as Router } from 'react-router-dom'; import { BrowserRouter as Router } from 'react-router-dom';
import configureStore from 'redux-mock-store'; import configureStore from 'redux-mock-store';
import { APP_NAME } from '../../../data/constants';
import { registerNewUser } from '../../data/actions'; import { registerNewUser } from '../../data/actions';
import { FIELDS } from '../../data/constants'; import { FIELDS } from '../../data/constants';
import { NOT_INITIALIZED } from '../../data/optimizelyExperiment/helper';
import useAutoGeneratedUsernameExperimentVariation
from '../../data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation';
import RegistrationPage from '../../RegistrationPage'; import RegistrationPage from '../../RegistrationPage';
import ConfigurableRegistrationForm from '../ConfigurableRegistrationForm'; import ConfigurableRegistrationForm from '../ConfigurableRegistrationForm';
@@ -22,9 +25,8 @@ jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'), ...jest.requireActual('@edx/frontend-platform/i18n'),
getLocale: jest.fn(), getLocale: jest.fn(),
})); }));
jest.mock('../../data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation', () => jest.fn());
const IntlConfigurableRegistrationForm = injectIntl(ConfigurableRegistrationForm);
const IntlRegistrationPage = injectIntl(RegistrationPage);
const mockStore = configureStore(); const mockStore = configureStore();
jest.mock('react-router-dom', () => { jest.mock('react-router-dom', () => {
@@ -94,6 +96,7 @@ describe('ConfigurableRegistrationForm', () => {
registrationFormData, registrationFormData,
usernameSuggestions: [], usernameSuggestions: [],
}, },
cohesion: { eventData: {} },
commonComponents: { commonComponents: {
thirdPartyAuthApiStatus: null, thirdPartyAuthApiStatus: null,
thirdPartyAuthContext, thirdPartyAuthContext,
@@ -121,6 +124,7 @@ describe('ConfigurableRegistrationForm', () => {
}; };
window.location = { search: '' }; window.location = { search: '' };
getLocale.mockImplementationOnce(() => ('en-us')); getLocale.mockImplementationOnce(() => ('en-us'));
useAutoGeneratedUsernameExperimentVariation.mockReturnValue(NOT_INITIALIZED);
}); });
afterEach(() => { afterEach(() => {
@@ -158,7 +162,7 @@ describe('ConfigurableRegistrationForm', () => {
}; };
render(routerWrapper(reduxWrapper( render(routerWrapper(reduxWrapper(
<IntlConfigurableRegistrationForm {...props} />, <ConfigurableRegistrationForm {...props} />,
))); )));
expect(document.querySelector('#profession')).toBeTruthy(); expect(document.querySelector('#profession')).toBeTruthy();
@@ -185,10 +189,11 @@ describe('ConfigurableRegistrationForm', () => {
}, },
}, },
autoSubmitRegistrationForm: true, autoSubmitRegistrationForm: true,
countriesCodesList: [{ code: 'AX', name: 'Åland Islands' }, { code: 'AL', name: 'Albania' }],
}; };
render(routerWrapper(reduxWrapper( render(routerWrapper(reduxWrapper(
<IntlConfigurableRegistrationForm {...props} />, <ConfigurableRegistrationForm {...props} />,
))); )));
expect(props.setFormFields).toHaveBeenCalledTimes(2); expect(props.setFormFields).toHaveBeenCalledTimes(2);
@@ -215,12 +220,12 @@ describe('ConfigurableRegistrationForm', () => {
}, },
}, },
}); });
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />))); render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
expect(document.querySelector('#profession')).toBeTruthy(); expect(document.querySelector('#profession')).toBeTruthy();
expect(document.querySelector('#tos')).toBeTruthy(); expect(document.querySelector('#tos')).toBeTruthy();
}); });
it('should submit form with fields returned by backend in payload', () => { it('should submit form with fields returned by backend in payload', async () => {
mergeConfig({ mergeConfig({
SHOW_CONFIGURABLE_EDX_FIELDS: true, SHOW_CONFIGURABLE_EDX_FIELDS: true,
}); });
@@ -245,11 +250,11 @@ describe('ConfigurableRegistrationForm', () => {
country: 'Pakistan', country: 'Pakistan',
honor_code: true, honor_code: true,
profession: 'Engineer', profession: 'Engineer',
totalRegistrationTime: 0, total_registration_time: 0,
}; };
store.dispatch = jest.fn(store.dispatch); store.dispatch = jest.fn(store.dispatch);
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />))); const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
populateRequiredFields(getByLabelText, payload); populateRequiredFields(getByLabelText, payload);
@@ -260,7 +265,9 @@ describe('ConfigurableRegistrationForm', () => {
fireEvent.click(submitButton); fireEvent.click(submitButton);
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK' })); await waitFor(() => {
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK', app_name: APP_NAME }));
});
}); });
it('should show error messages for required fields on empty form submission', () => { it('should show error messages for required fields on empty form submission', () => {
@@ -284,7 +291,7 @@ describe('ConfigurableRegistrationForm', () => {
}, },
}); });
const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />))); const { container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
const submitButton = container.querySelector('button.btn-brand'); const submitButton = container.querySelector('button.btn-brand');
fireEvent.click(submitButton); fireEvent.click(submitButton);
@@ -310,7 +317,7 @@ describe('ConfigurableRegistrationForm', () => {
}, },
}, },
}); });
const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />))); const { container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
const countryInput = container.querySelector('input[name="country"]'); const countryInput = container.querySelector('input[name="country"]');
fireEvent.change(countryInput, { target: { value: 'Pak', name: 'country' } }); fireEvent.change(countryInput, { target: { value: 'Pak', name: 'country' } });
fireEvent.blur(countryInput, { target: { value: 'Pak', name: 'country' } }); fireEvent.blur(countryInput, { target: { value: 'Pak', name: 'country' } });
@@ -335,7 +342,7 @@ describe('ConfigurableRegistrationForm', () => {
}, },
}, },
}); });
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />))); const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
const emailInput = getByLabelText('Email'); const emailInput = getByLabelText('Email');
const confirmEmailInput = getByLabelText('Confirm Email'); const confirmEmailInput = getByLabelText('Confirm Email');
@@ -356,7 +363,7 @@ describe('ConfigurableRegistrationForm', () => {
password: 'password1', password: 'password1',
country: 'Ukraine', country: 'Ukraine',
honor_code: true, honor_code: true,
totalRegistrationTime: 0, total_registration_time: 0,
}; };
store = mockStore({ store = mockStore({
@@ -371,7 +378,7 @@ describe('ConfigurableRegistrationForm', () => {
}, },
}, },
}); });
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />))); const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
populateRequiredFields(getByLabelText, formPayload, true); populateRequiredFields(getByLabelText, formPayload, true);
fireEvent.change( fireEvent.change(
@@ -406,7 +413,7 @@ describe('ConfigurableRegistrationForm', () => {
}); });
const { getByLabelText, container } = render( const { getByLabelText, container } = render(
routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)), routerWrapper(reduxWrapper(<RegistrationPage {...props} />)),
); );
const professionInput = getByLabelText('Profession'); const professionInput = getByLabelText('Profession');

View File

@@ -1,9 +1,8 @@
import React from 'react';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { mergeConfig } from '@edx/frontend-platform'; import { mergeConfig } from '@edx/frontend-platform';
import { import {
configure, getLocale, injectIntl, IntlProvider, configure, getLocale, IntlProvider,
} from '@edx/frontend-platform/i18n'; } from '@edx/frontend-platform/i18n';
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { BrowserRouter as Router } from 'react-router-dom'; import { BrowserRouter as Router } from 'react-router-dom';
@@ -12,6 +11,9 @@ import configureStore from 'redux-mock-store';
import { import {
FORBIDDEN_REQUEST, INTERNAL_SERVER_ERROR, TPA_AUTHENTICATION_FAILURE, TPA_SESSION_EXPIRED, FORBIDDEN_REQUEST, INTERNAL_SERVER_ERROR, TPA_AUTHENTICATION_FAILURE, TPA_SESSION_EXPIRED,
} from '../../data/constants'; } from '../../data/constants';
import { NOT_INITIALIZED } from '../../data/optimizelyExperiment/helper';
import useAutoGeneratedUsernameExperimentVariation
from '../../data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation';
import RegistrationPage from '../../RegistrationPage'; import RegistrationPage from '../../RegistrationPage';
import RegistrationFailureMessage from '../RegistrationFailure'; import RegistrationFailureMessage from '../RegistrationFailure';
@@ -23,9 +25,8 @@ jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'), ...jest.requireActual('@edx/frontend-platform/i18n'),
getLocale: jest.fn(), getLocale: jest.fn(),
})); }));
jest.mock('../../data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation', () => jest.fn());
const IntlRegistrationPage = injectIntl(RegistrationPage);
const IntlRegistrationFailure = injectIntl(RegistrationFailureMessage);
const mockStore = configureStore(); const mockStore = configureStore();
jest.mock('react-router-dom', () => { jest.mock('react-router-dom', () => {
@@ -95,6 +96,7 @@ describe('RegistrationFailure', () => {
registrationFormData, registrationFormData,
usernameSuggestions: [], usernameSuggestions: [],
}, },
cohesion: { eventData: {} },
commonComponents: { commonComponents: {
thirdPartyAuthApiStatus: null, thirdPartyAuthApiStatus: null,
thirdPartyAuthContext, thirdPartyAuthContext,
@@ -121,6 +123,7 @@ describe('RegistrationFailure', () => {
institutionLogin: false, institutionLogin: false,
}; };
window.location = { search: '' }; window.location = { search: '' };
useAutoGeneratedUsernameExperimentVariation.mockReturnValue(NOT_INITIALIZED);
}); });
afterEach(() => { afterEach(() => {
@@ -137,7 +140,7 @@ describe('RegistrationFailure', () => {
failureCount: 0, failureCount: 0,
}; };
const { container } = render(reduxWrapper(<IntlRegistrationFailure {...props} />)); const { container } = render(reduxWrapper(<RegistrationFailureMessage {...props} />));
const alertHeading = container.querySelectorAll('div.alert-heading'); const alertHeading = container.querySelectorAll('div.alert-heading');
expect(alertHeading.length).toEqual(1); expect(alertHeading.length).toEqual(1);
@@ -153,7 +156,7 @@ describe('RegistrationFailure', () => {
failureCount: 0, failureCount: 0,
}; };
const { container } = render(reduxWrapper(<IntlRegistrationFailure {...props} />)); const { container } = render(reduxWrapper(<RegistrationFailureMessage {...props} />));
const alertHeading = container.querySelectorAll('div.alert-heading'); const alertHeading = container.querySelectorAll('div.alert-heading');
expect(alertHeading.length).toEqual(1); expect(alertHeading.length).toEqual(1);
@@ -172,7 +175,7 @@ describe('RegistrationFailure', () => {
failureCount: 0, failureCount: 0,
}; };
const { container } = render(reduxWrapper(<IntlRegistrationFailure {...props} />)); const { container } = render(reduxWrapper(<RegistrationFailureMessage {...props} />));
const alertHeading = container.querySelectorAll('div.alert-heading'); const alertHeading = container.querySelectorAll('div.alert-heading');
expect(alertHeading.length).toEqual(1); expect(alertHeading.length).toEqual(1);
@@ -191,7 +194,7 @@ describe('RegistrationFailure', () => {
failureCount: 0, failureCount: 0,
}; };
const { container } = render(reduxWrapper(<IntlRegistrationFailure {...props} />)); const { container } = render(reduxWrapper(<RegistrationFailureMessage {...props} />));
const alertHeading = container.querySelectorAll('div.alert-heading'); const alertHeading = container.querySelectorAll('div.alert-heading');
expect(alertHeading.length).toEqual(1); expect(alertHeading.length).toEqual(1);
@@ -211,7 +214,7 @@ describe('RegistrationFailure', () => {
}, },
}); });
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />))); render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
const validationError = screen.queryByText('An error has occurred. Try refreshing the page, or check your internet connection.'); const validationError = screen.queryByText('An error has occurred. Try refreshing the page, or check your internet connection.');
expect(validationError).not.toBeNull(); expect(validationError).not.toBeNull();

View File

@@ -1,17 +1,20 @@
import React from 'react';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { getConfig, mergeConfig } from '@edx/frontend-platform'; import { getConfig, mergeConfig } from '@edx/frontend-platform';
import { import {
configure, getLocale, injectIntl, IntlProvider, configure, getLocale, IntlProvider,
} from '@edx/frontend-platform/i18n'; } from '@edx/frontend-platform/i18n';
import { fireEvent, render } from '@testing-library/react'; import { fireEvent, render, waitFor } from '@testing-library/react';
import { BrowserRouter as Router } from 'react-router-dom'; import { BrowserRouter as Router } from 'react-router-dom';
import configureStore from 'redux-mock-store'; import configureStore from 'redux-mock-store';
import mockTagular from '../../../cohesion/utils';
import { import {
COMPLETE_STATE, LOGIN_PAGE, PENDING_STATE, REGISTER_PAGE, COMPLETE_STATE, LOGIN_PAGE, PENDING_STATE, REGISTER_PAGE,
} from '../../../data/constants'; } from '../../../data/constants';
import { NOT_INITIALIZED } from '../../data/optimizelyExperiment/helper';
import useAutoGeneratedUsernameExperimentVariation
from '../../data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation';
import RegistrationPage from '../../RegistrationPage'; import RegistrationPage from '../../RegistrationPage';
jest.mock('@edx/frontend-platform/analytics', () => ({ jest.mock('@edx/frontend-platform/analytics', () => ({
@@ -22,8 +25,9 @@ jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'), ...jest.requireActual('@edx/frontend-platform/i18n'),
getLocale: jest.fn(), getLocale: jest.fn(),
})); }));
jest.mock('../../data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation', () => jest.fn());
mockTagular();
const IntlRegistrationPage = injectIntl(RegistrationPage);
const mockStore = configureStore(); const mockStore = configureStore();
jest.mock('react-router-dom', () => { jest.mock('react-router-dom', () => {
@@ -94,6 +98,7 @@ describe('ThirdPartyAuth', () => {
registrationFormData, registrationFormData,
usernameSuggestions: [], usernameSuggestions: [],
}, },
cohesion: { eventData: {} },
commonComponents: { commonComponents: {
thirdPartyAuthApiStatus: null, thirdPartyAuthApiStatus: null,
thirdPartyAuthContext, thirdPartyAuthContext,
@@ -120,6 +125,7 @@ describe('ThirdPartyAuth', () => {
institutionLogin: false, institutionLogin: false,
}; };
window.location = { search: '' }; window.location = { search: '' };
useAutoGeneratedUsernameExperimentVariation.mockReturnValue(NOT_INITIALIZED);
}); });
afterEach(() => { afterEach(() => {
@@ -157,7 +163,7 @@ describe('ThirdPartyAuth', () => {
}); });
const { queryByLabelText } = render( const { queryByLabelText } = render(
routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />, { store })), routerWrapper(reduxWrapper(<RegistrationPage {...props} />, { store })),
); );
const passwordField = queryByLabelText('Password'); const passwordField = queryByLabelText('Password');
@@ -182,7 +188,7 @@ describe('ThirdPartyAuth', () => {
window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: `?next=/dashboard&tpa_hint=${ssoProvider.id}` }; window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: `?next=/dashboard&tpa_hint=${ssoProvider.id}` };
const { container } = render( const { container } = render(
routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)), routerWrapper(reduxWrapper(<RegistrationPage {...props} />)),
); );
const tpaButton = container.querySelector(`button#${ssoProvider.id}`); const tpaButton = container.querySelector(`button#${ssoProvider.id}`);
@@ -207,7 +213,7 @@ describe('ThirdPartyAuth', () => {
search: `?next=/dashboard&tpa_hint=${ssoProvider.id}`, search: `?next=/dashboard&tpa_hint=${ssoProvider.id}`,
}; };
const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />))); const { container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
const skeletonElement = container.querySelector('.react-loading-skeleton'); const skeletonElement = container.querySelector('.react-loading-skeleton');
expect(skeletonElement).toBeTruthy(); expect(skeletonElement).toBeTruthy();
@@ -231,7 +237,7 @@ describe('ThirdPartyAuth', () => {
window.location = { href: getConfig().BASE_URL.concat(REGISTER_PAGE), search: `?next=/dashboard&tpa_hint=${ssoProvider.id}` }; window.location = { href: getConfig().BASE_URL.concat(REGISTER_PAGE), search: `?next=/dashboard&tpa_hint=${ssoProvider.id}` };
ssoProvider.iconImage = null; ssoProvider.iconImage = null;
const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />))); const { container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
const iconElement = container.querySelector(`button#${ssoProvider.id} div span.pgn__icon`); const iconElement = container.querySelector(`button#${ssoProvider.id} div span.pgn__icon`);
expect(iconElement).toBeTruthy(); expect(iconElement).toBeTruthy();
@@ -254,7 +260,7 @@ describe('ThirdPartyAuth', () => {
delete window.location; delete window.location;
window.location = { href: getConfig().BASE_URL.concat(REGISTER_PAGE), search: `?next=/dashboard&tpa_hint=${secondaryProviders.id}` }; window.location = { href: getConfig().BASE_URL.concat(REGISTER_PAGE), search: `?next=/dashboard&tpa_hint=${secondaryProviders.id}` };
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />))); render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
expect(window.location.href).toEqual(getConfig().LMS_BASE_URL + secondaryProviders.registerUrl); expect(window.location.href).toEqual(getConfig().LMS_BASE_URL + secondaryProviders.registerUrl);
}); });
@@ -275,7 +281,7 @@ describe('ThirdPartyAuth', () => {
delete window.location; delete window.location;
window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: '?next=/dashboard&tpa_hint=invalid' }; window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: '?next=/dashboard&tpa_hint=invalid' };
const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />))); const { container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
const providerButton = container.querySelector(`button#${ssoProvider.id} span#provider-name`); const providerButton = container.querySelector(`button#${ssoProvider.id} span#provider-name`);
expect(providerButton.textContent).toEqual(expectedMessage); expect(providerButton.textContent).toEqual(expectedMessage);
@@ -294,7 +300,7 @@ describe('ThirdPartyAuth', () => {
}); });
const { container } = render( const { container } = render(
routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />, { store })), routerWrapper(reduxWrapper(<RegistrationPage {...props} />, { store })),
); );
const buttonsWithId = container.querySelectorAll(`button#${ssoProvider.id}`); const buttonsWithId = container.querySelectorAll(`button#${ssoProvider.id}`);
@@ -315,7 +321,7 @@ describe('ThirdPartyAuth', () => {
}); });
const { container } = render( const { container } = render(
routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)), routerWrapper(reduxWrapper(<RegistrationPage {...props} />)),
); );
const buttonsWithId = container.querySelectorAll(`button#${ssoProvider.id}`); const buttonsWithId = container.querySelectorAll(`button#${ssoProvider.id}`);
@@ -329,12 +335,12 @@ describe('ThirdPartyAuth', () => {
institutionLogin: true, institutionLogin: true,
}; };
const { getByText } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />))); const { getByText } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
const headingElement = getByText('Register with institution/campus credentials'); const headingElement = getByText('Register with institution/campus credentials');
expect(headingElement).toBeTruthy(); expect(headingElement).toBeTruthy();
}); });
it('should redirect to social auth provider url on SSO button click', () => { it('should redirect to social auth provider url on SSO button click', async () => {
const registerUrl = '/auth/login/apple-id/?auth_entry=register&next=/dashboard'; const registerUrl = '/auth/login/apple-id/?auth_entry=register&next=/dashboard';
store = mockStore({ store = mockStore({
...initialState, ...initialState,
@@ -354,16 +360,18 @@ describe('ThirdPartyAuth', () => {
window.location = { href: getConfig().BASE_URL }; window.location = { href: getConfig().BASE_URL };
const { container } = render( const { container } = render(
routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)), routerWrapper(reduxWrapper(<RegistrationPage {...props} />)),
); );
const ssoButton = container.querySelector('button#oa2-apple-id'); const ssoButton = container.querySelector('button#oa2-apple-id');
fireEvent.click(ssoButton); fireEvent.click(ssoButton);
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + registerUrl); await waitFor(() => {
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + registerUrl);
}, { timeout: 1100 });
}); });
it('should redirect to finishAuthUrl upon successful registration via SSO', () => { it('should redirect to finishAuthUrl upon successful registration via SSO', async () => {
const authCompleteUrl = '/auth/complete/google-oauth2/'; const authCompleteUrl = '/auth/complete/google-oauth2/';
store = mockStore({ store = mockStore({
...initialState, ...initialState,
@@ -385,8 +393,10 @@ describe('ThirdPartyAuth', () => {
delete window.location; delete window.location;
window.location = { href: getConfig().BASE_URL }; window.location = { href: getConfig().BASE_URL };
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />))); render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + authCompleteUrl); await waitFor(() => {
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + authCompleteUrl);
}, { timeout: 1100 });
}); });
// ******** test alert messages ******** // ******** test alert messages ********
@@ -406,7 +416,7 @@ describe('ThirdPartyAuth', () => {
const expectedMessage = `${'You\'ve successfully signed into Apple! We just need a little more information before ' const expectedMessage = `${'You\'ve successfully signed into Apple! We just need a little more information before '
+ 'you start learning with '}${ getConfig().SITE_NAME }.`; + 'you start learning with '}${ getConfig().SITE_NAME }.`;
const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />))); const { container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
const tpaAlert = container.querySelector('#tpa-alert p'); const tpaAlert = container.querySelector('#tpa-alert p');
expect(tpaAlert.textContent).toEqual(expectedMessage); expect(tpaAlert.textContent).toEqual(expectedMessage);
}); });
@@ -437,7 +447,7 @@ describe('ThirdPartyAuth', () => {
store.dispatch = jest.fn(store.dispatch); store.dispatch = jest.fn(store.dispatch);
const { container } = render( const { container } = render(
routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)), routerWrapper(reduxWrapper(<RegistrationPage {...props} />)),
); );
const alertHeading = container.querySelector('div.alert-heading'); const alertHeading = container.querySelector('div.alert-heading');

View File

@@ -8,7 +8,7 @@ export const REGISTRATION_CLEAR_BACKEND_ERROR = 'REGISTRATION_CLEAR_BACKEND_ERRO
export const REGISTER_SET_COUNTRY_CODE = 'REGISTER_SET_COUNTRY_CODE'; export const REGISTER_SET_COUNTRY_CODE = 'REGISTER_SET_COUNTRY_CODE';
export const REGISTER_SET_USER_PIPELINE_DATA_LOADED = 'REGISTER_SET_USER_PIPELINE_DATA_LOADED'; export const REGISTER_SET_USER_PIPELINE_DATA_LOADED = 'REGISTER_SET_USER_PIPELINE_DATA_LOADED';
export const REGISTER_SET_EMAIL_SUGGESTIONS = 'REGISTER_SET_EMAIL_SUGGESTIONS'; export const REGISTER_SET_EMAIL_SUGGESTIONS = 'REGISTER_SET_EMAIL_SUGGESTIONS';
export const REGISTER_SET_AUTO_GENERATED_USERNAME_REGISTRATION_EXP_DATA = 'REGISTER_SET_AUTO_GENERATED_USERNAME_REGISTRATION_EXP_DATA';
// Backup registration form // Backup registration form
export const backupRegistrationForm = () => ({ export const backupRegistrationForm = () => ({
type: BACKUP_REGISTRATION_DATA.BASE, type: BACKUP_REGISTRATION_DATA.BASE,
@@ -83,3 +83,9 @@ export const setUserPipelineDataLoaded = (value) => ({
type: REGISTER_SET_USER_PIPELINE_DATA_LOADED, type: REGISTER_SET_USER_PIPELINE_DATA_LOADED,
payload: { value }, payload: { value },
}); });
// Auto Generated Username Registration Experiment Actions
export const setAutoGeneratedUsernameExperimentData = (autoGeneratedRegExpVariation) => ({
type: REGISTER_SET_AUTO_GENERATED_USERNAME_REGISTRATION_EXP_DATA,
payload: { autoGeneratedRegExpVariation },
});

View File

@@ -11,3 +11,4 @@ export const FORM_SUBMISSION_ERROR = 'form-submission-error';
export const INTERNAL_SERVER_ERROR = 'internal-server-error'; export const INTERNAL_SERVER_ERROR = 'internal-server-error';
export const TPA_AUTHENTICATION_FAILURE = 'tpa-authentication-failure'; export const TPA_AUTHENTICATION_FAILURE = 'tpa-authentication-failure';
export const TPA_SESSION_EXPIRED = 'tpa-session-expired'; export const TPA_SESSION_EXPIRED = 'tpa-session-expired';
export const FORBIDDEN_USERNAME = 'forbidden-username';

View File

@@ -0,0 +1,40 @@
import { useCallback } from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { useGoogleReCaptcha } from 'react-google-recaptcha-v3';
import messages from '../messages';
const useRecaptchaSubmission = (actionName = 'submit') => {
const { formatMessage } = useIntl();
const { executeRecaptcha } = useGoogleReCaptcha();
const recaptchaKey = getConfig().RECAPTCHA_SITE_KEY_WEB;
const isReady = !!executeRecaptcha || !recaptchaKey;
const executeWithFallback = useCallback(async () => {
if (executeRecaptcha && recaptchaKey) {
const token = await executeRecaptcha(actionName);
if (!token) {
throw new Error(formatMessage(messages['registration.captcha.verification.label']));
}
return token;
}
// Fallback: no reCAPTCHA or not ready
if (recaptchaKey) {
// eslint-disable-next-line no-console
console.warn(`reCAPTCHA not ready for action: ${actionName}. Proceeding without token.`);
}
return null;
}, [executeRecaptcha, recaptchaKey, actionName, formatMessage]);
return {
executeWithFallback,
isReady,
isLoading: recaptchaKey && !executeRecaptcha,
};
};
export default useRecaptchaSubmission;

View File

@@ -0,0 +1,90 @@
import { getConfig } from '@edx/frontend-platform';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { renderHook } from '@testing-library/react';
import { useGoogleReCaptcha } from 'react-google-recaptcha-v3';
import useRecaptchaSubmission from './hooks';
jest.mock('@edx/frontend-platform', () => ({
getConfig: jest.fn(),
}));
jest.mock('react-google-recaptcha-v3', () => ({
useGoogleReCaptcha: jest.fn(),
}));
jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'),
useIntl: () => ({ formatMessage: (msg) => msg.defaultMessage || msg }),
}));
describe('useRecaptchaSubmission', () => {
beforeEach(() => {
getConfig.mockReturnValue({ RECAPTCHA_SITE_KEY_WEB: 'test-key' });
useGoogleReCaptcha.mockReturnValue({ executeRecaptcha: jest.fn() });
});
it('should throw error if reCAPTCHA returns empty token', async () => {
useGoogleReCaptcha.mockReturnValue({
executeRecaptcha: jest.fn().mockResolvedValue(null),
});
const { result } = renderHook(() => useRecaptchaSubmission('test_action'), {
wrapper: ({ children }) => <IntlProvider locale="en">{children}</IntlProvider>,
});
await expect(result.current.executeWithFallback()).rejects.toThrow(
'CAPTCHA verification failed.',
);
});
it('should warn and return null if reCAPTCHA key exists but executeRecaptcha is not ready', async () => {
const warnSpy = jest.spyOn(console, 'warn').mockImplementation();
useGoogleReCaptcha.mockReturnValue({
executeRecaptcha: undefined,
});
const { result } = renderHook(() => useRecaptchaSubmission('test_action'), {
wrapper: ({ children }) => <IntlProvider locale="en">{children}</IntlProvider>,
});
const token = await result.current.executeWithFallback();
expect(token).toBeNull();
expect(warnSpy).toHaveBeenCalledWith(
'reCAPTCHA not ready for action: test_action. Proceeding without token.',
);
warnSpy.mockRestore();
});
it('should handle undefined RECAPTCHA_SITE_KEY_WEB gracefully', async () => {
const warnSpy = jest.spyOn(console, 'warn').mockImplementation();
getConfig.mockReturnValue({ RECAPTCHA_SITE_KEY_WEB: undefined });
const { result } = renderHook(() => useRecaptchaSubmission('test_action'), {
wrapper: ({ children }) => <IntlProvider locale="en">{children}</IntlProvider>,
});
const token = await result.current.executeWithFallback();
expect(token).toBeNull();
expect(warnSpy).not.toHaveBeenCalled();
warnSpy.mockRestore();
});
it('should return token if reCAPTCHA succeeds', async () => {
useGoogleReCaptcha.mockReturnValue({
executeRecaptcha: jest.fn().mockResolvedValue('valid-token'),
});
const { result } = renderHook(() => useRecaptchaSubmission('test_action'), {
wrapper: ({ children }) => <IntlProvider locale="en">{children}</IntlProvider>,
});
const token = await result.current.executeWithFallback();
expect(token).toBe('valid-token');
});
});

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