Compare commits

...

236 Commits

Author SHA1 Message Date
dependabot[bot]
c24cb89e2f build(deps): bump actions/checkout from 4 to 6
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-24 03:29:25 +00:00
renovate[bot]
34a334fabc chore(deps): update dependency @openedx/paragon to v23.18.0 (#1603)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-17 12:06:07 +00:00
renovate[bot]
1fd6e94792 chore(deps): update dependency @openedx/paragon to v23.17.0 (#1602)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-17 02:42:26 +00:00
renovate[bot]
e9b0902f49 fix(deps): update react-router monorepo to v6.30.2 (#1601)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-13 23:55:54 +00:00
renovate[bot]
a6ab635ea6 chore(deps): update dependency @openedx/paragon to v23.16.1 (#1600)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-12 21:48:30 +00:00
renovate[bot]
a22e0c80ca chore(deps): update dependency algoliasearch-helper to v3.26.1 (#1599)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-10 18:48:58 +00:00
renovate[bot]
cff8da5a5e chore(deps): update dependency algoliasearch to v4.25.3 (#1598)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-06 17:56:08 +00:00
renovate[bot]
70f11247d8 chore(deps): update dependency @openedx/paragon to v23.16.0 (#1597)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-03 19:12:38 +00:00
renovate[bot]
362a2962af chore(deps): update dependency @openedx/paragon to v23.15.2 (#1596)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-30 05:21:11 +00:00
renovate[bot]
80760103a2 chore(deps): update dependency @openedx/paragon to v23.15.1 (#1594)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-29 03:04:18 +00:00
Feanil Patel
bee0afd611 Merge pull request #1354 from open-craft/kshitij/plugin-slot
feat: Add plugin slot for login page
2025-10-24 09:53:15 -04:00
kshitij.sobti
0418a04fff feat: Add plugin slot for login page
This change adds a plugin slot for the login page allowing it to be customised.

Since there was a dependency conflict between frontend-plugin-framework and the react-hooks testing package, the react-hooks testing package has been removed and a replaced with a simple mechanism for testing hooks.

Since this touched the Login Page those have also been refactored to move away from redux connect.
2025-10-24 15:57:09 +05:30
renovate[bot]
5061391122 chore(deps): update dependency @edx/frontend-platform to v8.5.2 (#1593)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-24 04:10:39 +00:00
renovate[bot]
deb7cef005 chore(deps): update dependency @openedx/paragon to v23.15.0 (#1592)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-23 18:27:36 +00:00
renovate[bot]
9bd9d31599 fix(deps): update dependency redux-saga to v1.4.2 (#1591)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-21 22:53:05 +00:00
renovate[bot]
aec68e7c18 fix(deps): update dependency redux-saga to v1.4.1 (#1590)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-21 08:43:12 +00:00
renovate[bot]
f8868b1e36 chore(deps): update dependency @openedx/paragon to v23.14.9 (#1586)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-15 18:41:27 +00:00
Ihor Romaniuk
ffb8a2d434 fix: username suggestions alignment (#1584)
Co-authored-by: Kyrylo Hudym-Levkovych <kyr.hudym@kyrs-MacBook-Pro.local>
2025-10-15 01:51:39 +05:00
renovate[bot]
a615cba2fa chore(deps): update dependency @openedx/paragon to v23.14.8 (#1583)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-10 21:11:29 +00:00
renovate[bot]
c09d7f4eec chore(deps): update dependency ts-jest to v29.4.5 (#1582)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-10 18:04:03 +00:00
renovate[bot]
a67a08a5fb chore(deps): update dependency @openedx/paragon to v23.14.6 (#1581)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-10 13:10:59 +00:00
renovate[bot]
43ef53b703 chore(deps): update dependency babel-plugin-formatjs to v10.5.41 (#1580)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-09 18:05:55 +00:00
renovate[bot]
1dc39fcce1 chore(deps): update dependency @openedx/paragon to v23.14.5 (#1579)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-08 20:51:02 +00:00
renovate[bot]
0a28ef2fb4 chore(deps): update dependency babel-plugin-formatjs to v10.5.40 (#1578)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-06 01:36:40 +00:00
renovate[bot]
c2fa1fa2df chore(deps): update dependency @openedx/paragon to v23.14.4 (#1577)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-01 21:33:53 +00:00
renovate[bot]
44cf541b06 chore(deps): update dependency jest to v30.2.0 (#1576)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-29 01:01:52 +00:00
Feanil Patel
b5b12d0e87 Merge pull request #1574 from openedx/feanil/remove-reactifex-packages
build: remove unused @edx/reactifex package
2025-09-26 16:14:56 -04:00
Feanil Patel
b2862eeb42 build: remove unused @edx/reactifex package
Remove @edx/reactifex package from devDependencies as it is no longer
needed. Translation extraction functionality has been verified to work
correctly without this dependency.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-26 16:11:41 -04:00
renovate[bot]
92a333cc66 chore(deps): update dependency @openedx/paragon to v23.14.3 (#1575)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-26 16:10:20 -04:00
oleksandr.buhaienko
7a9d9bb300 test: Remove support for Node 20 2025-09-26 10:50:26 -03:00
oleksandr.buhaienko
fc4eb61ec9 build: Upgrade to Node 24 2025-09-26 09:17:49 -03:00
renovate[bot]
b2972929c9 chore(deps): update dependency ts-jest to v29.4.4 (#1573)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-19 18:00:14 +00:00
bydawen
bc251a61b2 test: Add Node 24 to CI matrix (#1564) 2025-09-19 13:56:48 -04:00
renovate[bot]
632e962161 chore(deps): update dependency ts-jest to v29.4.3 (#1572)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-18 10:35:42 +00:00
renovate[bot]
5d913b720e chore(deps): update dependency ts-jest to v29.4.2 (#1570)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-16 10:17:33 +00: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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
Stanislav Lunyachek
3ddaf795f2 feat: Hide preloaders for third party auth providers if they are disabled 2024-04-19 10:55:48 +05:00
renovate[bot]
a18df02d37 fix(deps): update dependency algoliasearch-helper to v3.17.0 2024-04-11 09:20:07 +00:00
72 changed files with 19953 additions and 13933 deletions

3
.env
View File

@@ -23,6 +23,7 @@ POST_REGISTRATION_REDIRECT_URL=''
SEARCH_CATALOG_URL=''
# ***** Features flags *****
DISABLE_ENTERPRISE_LOGIN=''
ENABLE_AUTO_GENERATED_USERNAME=''
ENABLE_DYNAMIC_REGISTRATION_FIELDS=''
ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN=''
ENABLE_POST_REGISTRATION_RECOMMENDATIONS=''
@@ -40,3 +41,5 @@ BANNER_IMAGE_EXTRA_SMALL=''
# ***** Miscellaneous *****
APP_ID=''
MFE_CONFIG_API_URL=''
# Fallback in local style files
PARAGON_THEME_URLS={}

View File

@@ -41,3 +41,5 @@ APP_ID=''
MFE_CONFIG_API_URL=''
ZENDESK_KEY=''
ZENDESK_LOGO_URL=''
# Fallback in local style files
PARAGON_THEME_URLS={}

View File

@@ -18,3 +18,4 @@ SEGMENT_KEY=''
SITE_NAME='Your Platform Name Here'
APP_ID=''
MFE_CONFIG_API_URL=''
PARAGON_THEME_URLS={}

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
* [ ] 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.

View File

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

View File

@@ -10,17 +10,15 @@ on:
jobs:
tests:
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Nodejs Env
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
uses: actions/checkout@v6
- name: Setup Nodejs
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VER }}
node-version-file: '.nvmrc'
- name: Install Dependencies
run: npm ci
@@ -41,4 +39,7 @@ jobs:
run: npm run build
- 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:
version-check:
uses: openedx/.github/.github/workflows/lockfile-check.yml@master
uses: openedx/.github/.github/workflows/lockfileversion-check-v3.yml@master

2
.nvmrc
View File

@@ -1 +1 @@
18
24

View File

@@ -1,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
============
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.
@@ -51,7 +57,7 @@ This MFE is bundled with `Devstack <https://github.com/openedx/devstack>`_, see
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:
@@ -142,13 +148,13 @@ Furthermore, there are several edX-specific environment variables that enable in
- ``true`` | ``''`` (empty strings are falsy)
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
=================
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.
@@ -187,7 +193,7 @@ All community members are expected to follow the `Open edX Code of Conduct <http
People
======
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.
Reporting Security Issues

View File

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

View File

@@ -3,7 +3,7 @@ Enable Social Auth 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.

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

32170
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -13,15 +13,12 @@
"build": "fedx-scripts webpack",
"i18n_extract": "fedx-scripts formatjs extract",
"lint": "fedx-scripts eslint --ext .js --ext .jsx .",
"lint:fix": "fedx-scripts eslint --fix --ext .js --ext .jsx .",
"snapshot": "fedx-scripts jest --updateSnapshot",
"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"
},
"husky": {
"hooks": {
"pre-commit": "npm run lint"
}
},
"author": "edX",
"license": "AGPL-3.0",
"homepage": "https://github.com/openedx/frontend-app-authn#readme",
@@ -33,53 +30,52 @@
},
"dependencies": {
"@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",
"@fortawesome/fontawesome-svg-core": "6.5.2",
"@fortawesome/free-brands-svg-icons": "6.5.2",
"@fortawesome/free-solid-svg-icons": "6.5.2",
"@fortawesome/react-fontawesome": "0.2.0",
"@openedx/paragon": "^22.1.1",
"@fortawesome/fontawesome-svg-core": "6.7.2",
"@fortawesome/free-brands-svg-icons": "6.7.2",
"@fortawesome/free-solid-svg-icons": "6.7.2",
"@fortawesome/react-fontawesome": "0.2.6",
"@openedx/frontend-plugin-framework": "^1.7.0",
"@openedx/paragon": "^23.4.2",
"@optimizely/react-sdk": "^2.9.1",
"@redux-devtools/extension": "3.3.0",
"@testing-library/react": "^12.1.5",
"@testing-library/react-hooks": "^8.0.1",
"@testing-library/react": "^16.2.0",
"algoliasearch": "^4.14.3",
"algoliasearch-helper": "^3.14.0",
"algoliasearch-helper": "^3.26.0",
"classnames": "2.5.1",
"core-js": "3.36.1",
"core-js": "3.43.0",
"fastest-levenshtein": "1.0.16",
"form-urlencoded": "6.1.4",
"form-urlencoded": "6.1.6",
"prop-types": "15.8.1",
"query-string": "7.1.3",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-helmet": "6.1.0",
"react-loading-skeleton": "3.4.0",
"react-loading-skeleton": "3.5.0",
"react-redux": "7.2.9",
"react-responsive": "8.2.0",
"react-router": "6.22.3",
"react-router-dom": "6.22.3",
"react-router": "6.30.2",
"react-router-dom": "6.30.2",
"react-zendesk": "^0.1.13",
"redux": "4.2.0",
"redux": "4.2.1",
"redux-logger": "3.0.6",
"redux-mock-store": "1.5.4",
"redux-saga": "1.3.0",
"redux-mock-store": "1.5.5",
"redux-saga": "1.4.2",
"redux-thunk": "2.4.2",
"regenerator-runtime": "0.14.1",
"reselect": "4.1.8",
"universal-cookie": "4.0.4"
"reselect": "5.1.1",
"universal-cookie": "7.2.2"
},
"devDependencies": {
"@edx/browserslist-config": "^1.1.1",
"@edx/reactifex": "1.1.0",
"@openedx/frontend-build": "13.1.4",
"babel-plugin-formatjs": "10.5.14",
"eslint-plugin-import": "2.29.1",
"@openedx/frontend-build": "^14.6.2",
"babel-plugin-formatjs": "10.5.41",
"eslint-plugin-import": "2.32.0",
"glob": "7.2.3",
"history": "5.3.0",
"husky": "7.0.4",
"jest": "29.7.0",
"react-test-renderer": "^17.0.2"
"jest": "30.2.0",
"react-test-renderer": "^18.3.1",
"ts-jest": "^29.4.0"
}
}

View File

@@ -5,8 +5,8 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" href="<%=htmlWebpackPlugin.options.FAVICON_URL%>" type="image/x-icon"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/iframe-resizer/4.3.9/iframeResizer.contentWindow.min.js"
integrity="sha512-mdT/HQRzoRP4laVz49Mndx6rcCGA3IhuyhP3gaY0E9sZPkwbtDk9ttQIq9o8qGCf5VvJv1Xsy3k2yTjfUoczqw=="
<script src="https://cdnjs.cloudflare.com/ajax/libs/iframe-resizer/4.4.4/iframeResizer.contentWindow.min.js"
integrity="sha512-IWwZFBvHzN41wNI6etRLLuLrDDj/6AwJcPt7cmKJAzluYTIHHQ1PF8wh0rSy05jxEvvjflVvH2MxeV6riyEEXg=="
crossorigin="anonymous"
referrerpolicy="no-referrer">
</script>

View File

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

View File

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

View File

@@ -1,7 +1,6 @@
import React from 'react';
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 { act } from 'react-dom/test-utils';
import { MemoryRouter } from 'react-router-dom';
@@ -37,7 +36,6 @@ describe('FormGroup', () => {
describe('PasswordField', () => {
const mockStore = configureStore();
const IntlPasswordField = injectIntl(PasswordField);
let props = {};
let store = {};
@@ -66,7 +64,7 @@ describe('PasswordField', () => {
});
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 showPasswordButton = getByLabelText('Show password');
@@ -79,7 +77,7 @@ describe('PasswordField', () => {
});
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');
jest.useFakeTimers();
await act(async () => {
@@ -96,7 +94,7 @@ describe('PasswordField', () => {
...props,
value: '',
};
const { getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />));
const { getByLabelText } = render(reduxWrapper(<PasswordField {...props} />));
const passwordInput = getByLabelText('Password');
jest.useFakeTimers();
await act(async () => {
@@ -119,7 +117,7 @@ describe('PasswordField', () => {
});
it('should update password requirement checks', async () => {
const { getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />));
const { getByLabelText } = render(reduxWrapper(<PasswordField {...props} />));
const passwordInput = getByLabelText('Password');
jest.useFakeTimers();
await act(async () => {
@@ -142,7 +140,7 @@ describe('PasswordField', () => {
});
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 passwordIcon = getByLabelText('Show password');
@@ -163,7 +161,7 @@ describe('PasswordField', () => {
...props,
handleBlur: jest.fn(),
};
const { container } = render(reduxWrapper(<IntlPasswordField {...props} />));
const { container } = render(reduxWrapper(<PasswordField {...props} />));
const passwordInput = container.querySelector('input[name="password"]');
fireEvent.blur(passwordInput, {
@@ -181,7 +179,7 @@ describe('PasswordField', () => {
...props,
handleErrorChange: jest.fn(),
};
const { container } = render(reduxWrapper(<IntlPasswordField {...props} />));
const { container } = render(reduxWrapper(<PasswordField {...props} />));
const passwordInput = container.querySelector('input[name="password"]');
fireEvent.blur(passwordInput, {
@@ -204,7 +202,7 @@ describe('PasswordField', () => {
handleErrorChange: jest.fn(),
};
const { getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />));
const { getByLabelText } = render(reduxWrapper(<PasswordField {...props} />));
const passwordIcon = getByLabelText('Show password');
@@ -224,7 +222,7 @@ describe('PasswordField', () => {
handleErrorChange: jest.fn(),
};
const { getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />));
const { getByLabelText } = render(reduxWrapper(<PasswordField {...props} />));
const passwordIcon = getByLabelText('Show password');
@@ -248,7 +246,7 @@ describe('PasswordField', () => {
...props,
handleErrorChange: jest.fn(),
};
const { getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />));
const { getByLabelText } = render(reduxWrapper(<PasswordField {...props} />));
const passwordField = getByLabelText('Password');
fireEvent.blur(passwordField, {
target: {
@@ -268,7 +266,7 @@ describe('PasswordField', () => {
handleErrorChange: jest.fn(),
handleBlur: jest.fn(),
};
const { getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />));
const { getByLabelText } = render(reduxWrapper(<PasswordField {...props} />));
const passwordIcon = getByLabelText('Show password');

View File

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

View File

@@ -66,14 +66,14 @@ exports[`SocialAuthProviders should match social auth provider with iconClass sn
data-prefix="fab"
focusable="false"
role="img"
style={Object {}}
style={{}}
viewBox="0 0 488 512"
xmlns="http://www.w3.org/2000/svg"
>
<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"
fill="currentColor"
style={Object {}}
style={{}}
/>
</svg>
</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`] = `
Array [
[
<button
className="btn-social btn-oa2-apple-id mr-3"
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`] = `
Array [
[
<div
className="fade alert-content alert-success mt-n2 mb-5 alert show"
id="tpa-alert"

View File

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

View File

@@ -4,6 +4,7 @@ const configuration = {
USER_RETENTION_COOKIE_NAME: process.env.USER_RETENTION_COOKIE_NAME || '',
// Features
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_PROGRESSIVE_PROFILING_ON_AUTHN: process.env.ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN || false,
ENABLE_POST_REGISTRATION_RECOMMENDATIONS: process.env.ENABLE_POST_REGISTRATION_RECOMMENDATIONS || false,

View File

@@ -1,8 +1,7 @@
import React from 'react';
import { Provider } from 'react-redux';
import { mergeConfig } from '@edx/frontend-platform';
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';
@@ -26,7 +25,6 @@ jest.mock('react-router-dom', () => ({
useNavigate: () => mockedNavigator,
}));
const IntlForgotPasswordPage = injectIntl(ForgotPasswordPage);
const mockStore = configureStore();
const initialState = {
@@ -78,7 +76,7 @@ describe('ForgotPasswordPage', () => {
);
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');
expect(forgotPasswordButton).toBeNull();
});
@@ -87,14 +85,14 @@ describe('ForgotPasswordPage', () => {
mergeConfig({
LOGIN_ISSUE_SUPPORT_LINK: '/support',
});
render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
render(reduxWrapper(<ForgotPasswordPage {...props} />));
const forgotPasswordButton = screen.findByText('Need help signing in?');
expect(forgotPasswordButton).toBeDefined();
});
it('should display email validation error message', async () => {
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');
@@ -115,7 +113,7 @@ describe('ForgotPasswordPage', () => {
const expectedMessage = 'We were unable to contact you.'
+ '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 validationErrors = alertElements[0].textContent;
@@ -124,7 +122,7 @@ describe('ForgotPasswordPage', () => {
it('should display empty email validation message', async () => {
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');
fireEvent.click(submitButton);
@@ -141,7 +139,7 @@ describe('ForgotPasswordPage', () => {
forgotPassword: { status: 'forbidden' },
});
const { container } = render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
const { container } = render(reduxWrapper(<ForgotPasswordPage {...props} />));
const alertElements = container.querySelectorAll('.alert-danger');
const validationErrors = alertElements[0].textContent;
@@ -149,7 +147,7 @@ describe('ForgotPasswordPage', () => {
});
it('should not display any error message on change event', () => {
render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
render(reduxWrapper(<ForgotPasswordPage {...props} />));
const emailInput = screen.getByLabelText('Email');
@@ -172,7 +170,7 @@ describe('ForgotPasswordPage', () => {
};
store.dispatch = jest.fn(store.dispatch);
render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
render(reduxWrapper(<ForgotPasswordPage {...props} />));
const emailInput = screen.getByLabelText('Email');
fireEvent.blur(emailInput);
@@ -187,7 +185,7 @@ describe('ForgotPasswordPage', () => {
emailValidationError: validationMessage,
email: '',
};
const { container } = render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
const { container } = render(reduxWrapper(<ForgotPasswordPage {...props} />));
const validationElement = container.querySelector('.pgn__form-text-invalid');
expect(validationElement.textContent).toEqual(validationMessage);
});
@@ -205,7 +203,7 @@ describe('ForgotPasswordPage', () => {
store.dispatch = jest.fn(store.dispatch);
render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
render(reduxWrapper(<ForgotPasswordPage {...props} />));
const emailInput = screen.getByLabelText('Email');
fireEvent.focus(emailInput);
@@ -219,7 +217,7 @@ describe('ForgotPasswordPage', () => {
emailValidationError: '',
email: '',
};
render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
render(reduxWrapper(<ForgotPasswordPage {...props} />));
const errorElement = screen.queryByTestId('email-invalid-feedback');
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,'
+ ' 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);
expect(successElement).toBeDefined();
@@ -254,7 +252,7 @@ describe('ForgotPasswordPage', () => {
+ 'This password reset link is invalid. It may have been used already. '
+ '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);
expect(successElement).toBeDefined();
@@ -262,7 +260,7 @@ describe('ForgotPasswordPage', () => {
});
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 anchorElement = navElement.querySelector('a');

View File

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

View File

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

View File

@@ -1,26 +1,17 @@
import React, { useEffect, useMemo, useState } from 'react';
import { connect } from 'react-redux';
import {
useCallback, useEffect, useMemo, useState,
} from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { getConfig } from '@edx/frontend-platform';
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
import { injectIntl, useIntl } from '@edx/frontend-platform/i18n';
import {
Form, StatefulButton,
} from '@openedx/paragon';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Form, StatefulButton } from '@openedx/paragon';
import PropTypes from 'prop-types';
import { Helmet } from 'react-helmet';
import Skeleton from 'react-loading-skeleton';
import { Link } from 'react-router-dom';
import AccountActivationMessage from './AccountActivationMessage';
import {
backupLoginFormBegin,
dismissPasswordResetBanner,
loginRequest,
} from './data/actions';
import { INVALID_FORM, TPA_AUTHENTICATION_FAILURE } from './data/constants';
import LoginFailureMessage from './LoginFailure';
import messages from './messages';
import {
FormGroup,
InstitutionLogistration,
@@ -28,13 +19,12 @@ import {
RedirectLogistration,
ThirdPartyAuthAlert,
} from '../common-components';
import AccountActivationMessage from './AccountActivationMessage';
import { getThirdPartyAuthContext } from '../common-components/data/actions';
import { thirdPartyAuthContextSelector } from '../common-components/data/selectors';
import EnterpriseSSO from '../common-components/EnterpriseSSO';
import ThirdPartyAuth from '../common-components/ThirdPartyAuth';
import {
DEFAULT_STATE, PENDING_STATE, RESET_PAGE,
} from '../data/constants';
import { PENDING_STATE, RESET_PAGE } from '../data/constants';
import {
getActivationStatus,
getAllPossibleQueryParams,
@@ -43,37 +33,57 @@ import {
updatePathWithQueryParams,
} from '../data/utils';
import ResetPasswordSuccess from '../reset-password/ResetPasswordSuccess';
import { backupLoginFormBegin, dismissPasswordResetBanner, loginRequest } from './data/actions';
import { INVALID_FORM, TPA_AUTHENTICATION_FAILURE } from './data/constants';
import LoginFailureMessage from './LoginFailure';
import messages from './messages';
const LoginPage = (props) => {
const LoginPage = ({
institutionLogin,
handleInstitutionLogin,
}) => {
const dispatch = useDispatch();
const backupFormState = useCallback((data) => dispatch(backupLoginFormBegin(data)), [dispatch]);
const getTPADataFromBackend = useCallback(() => dispatch(getThirdPartyAuthContext()), [dispatch]);
const {
backedUpFormData,
loginErrorCode,
loginErrorContext,
loginResult,
shouldBackupState,
thirdPartyAuthContext: {
providers,
currentProvider,
secondaryProviders,
finishAuthUrl,
platformName,
errorMessage: thirdPartyErrorMessage,
},
thirdPartyAuthApiStatus,
institutionLogin,
showResetPasswordSuccessBanner,
submitState,
// Actions
backupFormState,
handleInstitutionLogin,
getTPADataFromBackend,
} = props;
thirdPartyAuthContext,
thirdPartyAuthApiStatus,
} = useSelector((state) => ({
backedUpFormData: state.login.loginFormData,
loginErrorCode: state.login.loginErrorCode,
loginErrorContext: state.login.loginErrorContext,
loginResult: state.login.loginResult,
shouldBackupState: state.login.shouldBackupState,
showResetPasswordSuccessBanner: state.login.showResetPasswordSuccessBanner,
submitState: state.login.submitState,
thirdPartyAuthContext: thirdPartyAuthContextSelector(state),
thirdPartyAuthApiStatus: state.commonComponents.thirdPartyAuthApiStatus,
}));
const {
providers,
currentProvider,
secondaryProviders,
finishAuthUrl,
platformName,
errorMessage: thirdPartyErrorMessage,
} = thirdPartyAuthContext;
const { formatMessage } = useIntl();
const activationMsgType = getActivationStatus();
const queryParams = useMemo(() => getAllPossibleQueryParams(), []);
const [formFields, setFormFields] = useState({ ...backedUpFormData.formFields });
const [errorCode, setErrorCode] = useState({ type: '', count: 0, context: {} });
const [errorCode, setErrorCode] = useState({
type: '',
count: 0,
context: {},
});
const [errors, setErrors] = useState({ ...backedUpFormData.errors });
const tpaHint = getTpaHint();
@@ -87,7 +97,7 @@ const LoginPage = (props) => {
payload.tpa_hint = tpaHint;
}
getTPADataFromBackend(payload);
}, [getTPADataFromBackend, queryParams, tpaHint]);
}, [queryParams, tpaHint, getTPADataFromBackend]);
/**
* Backup the login form in redux when login page is toggled.
*/
@@ -98,7 +108,7 @@ const LoginPage = (props) => {
errors: { ...errors },
});
}
}, [shouldBackupState, formFields, errors, backupFormState]);
}, [backupFormState, shouldBackupState, formFields, errors]);
useEffect(() => {
if (loginErrorCode) {
@@ -123,7 +133,10 @@ const LoginPage = (props) => {
}, [thirdPartyErrorMessage]);
const validateFormFields = (payload) => {
const { emailOrUsername, password } = payload;
const {
emailOrUsername,
password,
} = payload;
const fieldErrors = { ...errors };
if (emailOrUsername === '') {
@@ -141,14 +154,18 @@ const LoginPage = (props) => {
const handleSubmit = (event) => {
event.preventDefault();
if (showResetPasswordSuccessBanner) {
props.dismissPasswordResetBanner();
dispatch(dismissPasswordResetBanner());
}
const formData = { ...formFields };
const validationErrors = validateFormFields(formData);
if (validationErrors.emailOrUsername || validationErrors.password) {
setErrors({ ...validationErrors });
setErrorCode(prevState => ({ type: INVALID_FORM, count: prevState.count + 1, context: {} }));
setErrorCode(prevState => ({
type: INVALID_FORM,
count: prevState.count + 1,
context: {},
}));
return;
}
@@ -158,23 +175,35 @@ const LoginPage = (props) => {
password: formData.password,
...queryParams,
};
props.loginRequest(payload);
dispatch(loginRequest(payload));
};
const handleOnChange = (event) => {
const { name, value } = event.target;
setFormFields(prevState => ({ ...prevState, [name]: value }));
const {
name,
value,
} = event.target;
setFormFields(prevState => ({
...prevState,
[name]: value,
}));
};
const handleOnFocus = (event) => {
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);
if (tpaHint) {
if (thirdPartyAuthApiStatus === PENDING_STATE) {
@@ -281,88 +310,9 @@ const LoginPage = (props) => {
);
};
const mapStateToProps = state => {
const loginPageState = state.login;
return {
backedUpFormData: loginPageState.loginFormData,
loginErrorCode: loginPageState.loginErrorCode,
loginErrorContext: loginPageState.loginErrorContext,
loginResult: loginPageState.loginResult,
shouldBackupState: loginPageState.shouldBackupState,
showResetPasswordSuccessBanner: loginPageState.showResetPasswordSuccessBanner,
submitState: loginPageState.submitState,
thirdPartyAuthContext: thirdPartyAuthContextSelector(state),
thirdPartyAuthApiStatus: state.commonComponents.thirdPartyAuthApiStatus,
};
};
LoginPage.propTypes = {
backedUpFormData: PropTypes.shape({
formFields: PropTypes.shape({}),
errors: PropTypes.shape({}),
}),
loginErrorCode: PropTypes.string,
loginErrorContext: PropTypes.shape({
email: PropTypes.string,
redirectUrl: PropTypes.string,
context: PropTypes.shape({}),
}),
loginResult: PropTypes.shape({
redirectUrl: PropTypes.string,
success: PropTypes.bool,
}),
shouldBackupState: PropTypes.bool,
showResetPasswordSuccessBanner: PropTypes.bool,
submitState: PropTypes.string,
thirdPartyAuthApiStatus: PropTypes.string,
institutionLogin: PropTypes.bool.isRequired,
thirdPartyAuthContext: PropTypes.shape({
currentProvider: PropTypes.string,
errorMessage: PropTypes.string,
platformName: PropTypes.string,
providers: PropTypes.arrayOf(PropTypes.shape({})),
secondaryProviders: PropTypes.arrayOf(PropTypes.shape({})),
finishAuthUrl: PropTypes.string,
}),
// Actions
backupFormState: PropTypes.func.isRequired,
dismissPasswordResetBanner: PropTypes.func.isRequired,
loginRequest: PropTypes.func.isRequired,
getTPADataFromBackend: PropTypes.func.isRequired,
handleInstitutionLogin: PropTypes.func.isRequired,
};
LoginPage.defaultProps = {
backedUpFormData: {
formFields: {
emailOrUsername: '', password: '',
},
errors: {
emailOrUsername: '', password: '',
},
},
loginErrorCode: null,
loginErrorContext: {},
loginResult: {},
shouldBackupState: false,
showResetPasswordSuccessBanner: false,
submitState: DEFAULT_STATE,
thirdPartyAuthApiStatus: PENDING_STATE,
thirdPartyAuthContext: {
currentProvider: null,
errorMessage: null,
finishAuthUrl: null,
providers: [],
secondaryProviders: [],
},
};
export default connect(
mapStateToProps,
{
backupFormState: backupLoginFormBegin,
dismissPasswordResetBanner,
loginRequest,
getTPADataFromBackend: getThirdPartyAuthContext,
},
)(injectIntl(LoginPage));
export default LoginPage;

View File

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

View File

@@ -1,7 +1,5 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import {
fireEvent, render, screen,
} from '@testing-library/react';
@@ -11,7 +9,6 @@ import { MemoryRouter } from 'react-router-dom';
import { RESET_PAGE } from '../../data/constants';
import ChangePasswordPrompt from '../ChangePasswordPrompt';
const IntlChangePasswordPrompt = injectIntl(ChangePasswordPrompt);
const mockedNavigator = jest.fn();
jest.mock('react-router-dom', () => ({
@@ -44,7 +41,7 @@ describe('ChangePasswordPromptTests', () => {
render(
<IntlProvider locale="en">
<MemoryRouter>
<IntlChangePasswordPrompt {...props} />
<ChangePasswordPrompt {...props} />
</MemoryRouter>
</IntlProvider>,
);
@@ -61,7 +58,7 @@ describe('ChangePasswordPromptTests', () => {
render(
<IntlProvider locale="en">
<MemoryRouter>
<IntlChangePasswordPrompt {...props} />
<ChangePasswordPrompt {...props} />
</MemoryRouter>
</IntlProvider>,
);

View File

@@ -1,6 +1,4 @@
import React from 'react';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import {
render, screen,
} from '@testing-library/react';
@@ -26,8 +24,6 @@ jest.mock('@edx/frontend-platform/auth', () => ({
getAuthService: jest.fn(),
}));
const IntlLoginFailureMessage = injectIntl(LoginFailureMessage);
describe('LoginFailureMessage', () => {
let props = {};
@@ -48,7 +44,7 @@ describe('LoginFailureMessage', () => {
render(
<IntlProvider locale="en">
<IntlLoginFailureMessage {...props} />
<LoginFailureMessage {...props} />
</IntlProvider>,
);
@@ -76,7 +72,7 @@ describe('LoginFailureMessage', () => {
render(
<IntlProvider locale="en">
<IntlLoginFailureMessage {...props} />
<LoginFailureMessage {...props} />
</IntlProvider>,
);
@@ -106,7 +102,7 @@ describe('LoginFailureMessage', () => {
render(
<IntlProvider locale="en">
<IntlLoginFailureMessage {...props} />
<LoginFailureMessage {...props} />
</IntlProvider>,
);
@@ -132,7 +128,7 @@ describe('LoginFailureMessage', () => {
render(
<IntlProvider locale="en">
<IntlLoginFailureMessage {...props} />
<LoginFailureMessage {...props} />
</IntlProvider>,
);
@@ -152,7 +148,7 @@ describe('LoginFailureMessage', () => {
render(
<IntlProvider locale="en">
<IntlLoginFailureMessage {...props} />
<LoginFailureMessage {...props} />
</IntlProvider>,
);
@@ -176,7 +172,7 @@ describe('LoginFailureMessage', () => {
render(
<IntlProvider locale="en">
<IntlLoginFailureMessage {...props} />
<LoginFailureMessage {...props} />
</IntlProvider>,
);
@@ -196,7 +192,7 @@ describe('LoginFailureMessage', () => {
render(
<IntlProvider locale="en">
<IntlLoginFailureMessage {...props} />
<LoginFailureMessage {...props} />
</IntlProvider>,
);
@@ -216,7 +212,7 @@ describe('LoginFailureMessage', () => {
render(
<IntlProvider locale="en">
<IntlLoginFailureMessage {...props} />
<LoginFailureMessage {...props} />
</IntlProvider>,
);
@@ -236,7 +232,7 @@ describe('LoginFailureMessage', () => {
render(
<IntlProvider locale="en">
<IntlLoginFailureMessage {...props} />
<LoginFailureMessage {...props} />
</IntlProvider>,
);
@@ -255,7 +251,7 @@ describe('LoginFailureMessage', () => {
render(
<IntlProvider locale="en">
<IntlLoginFailureMessage {...props} />
<LoginFailureMessage {...props} />
</IntlProvider>,
);
@@ -275,7 +271,7 @@ describe('LoginFailureMessage', () => {
render(
<IntlProvider locale="en">
<IntlLoginFailureMessage {...props} />
<LoginFailureMessage {...props} />
</IntlProvider>,
);
@@ -301,7 +297,7 @@ describe('LoginFailureMessage', () => {
render(
<IntlProvider locale="en">
<MemoryRouter>
<IntlLoginFailureMessage {...props} />
<LoginFailureMessage {...props} />
</MemoryRouter>
</IntlProvider>,
);
@@ -327,7 +323,7 @@ describe('LoginFailureMessage', () => {
render(
<IntlProvider locale="en">
<MemoryRouter>
<IntlLoginFailureMessage {...props} />
<LoginFailureMessage {...props} />
</MemoryRouter>
</IntlProvider>,
);
@@ -359,7 +355,7 @@ describe('LoginFailureMessage', () => {
render(
<IntlProvider locale="en">
<IntlLoginFailureMessage {...props} />
<LoginFailureMessage {...props} />
</IntlProvider>,
);

View File

@@ -1,9 +1,8 @@
import React from 'react';
import { Provider } from 'react-redux';
import { getConfig, mergeConfig } from '@edx/frontend-platform';
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import {
fireEvent, render, screen, waitFor,
} from '@testing-library/react';
@@ -24,7 +23,6 @@ jest.mock('@edx/frontend-platform/auth', () => ({
getAuthService: jest.fn(),
}));
const IntlLoginPage = injectIntl(LoginPage);
const mockStore = configureStore();
describe('LoginPage', () => {
@@ -43,6 +41,14 @@ describe('LoginPage', () => {
const initialState = {
login: {
loginResult: { success: false, redirectUrl: '' },
loginFormData: {
formFields: {
emailOrUsername: '', password: '',
},
errors: {
emailOrUsername: '', password: '',
},
},
},
commonComponents: {
thirdPartyAuthApiStatus: null,
@@ -88,7 +94,7 @@ describe('LoginPage', () => {
it('should submit form for valid input', () => {
store.dispatch = jest.fn(store.dispatch);
render(reduxWrapper(<IntlLoginPage {...props} />));
render(reduxWrapper(<LoginPage {...props} />));
fireEvent.change(screen.getByText(
'',
@@ -109,7 +115,7 @@ describe('LoginPage', () => {
it('should not dispatch loginRequest on empty form submission', () => {
store.dispatch = jest.fn(store.dispatch);
render(reduxWrapper(<IntlLoginPage {...props} />));
render(reduxWrapper(<LoginPage {...props} />));
fireEvent.click(screen.getByText(
'',
@@ -128,7 +134,7 @@ describe('LoginPage', () => {
});
store.dispatch = jest.fn(store.dispatch);
render(reduxWrapper(<IntlLoginPage {...props} />));
render(reduxWrapper(<LoginPage {...props} />));
fireEvent.click(screen.getByText(
'',
{ selector: '.btn-brand' },
@@ -142,7 +148,7 @@ describe('LoginPage', () => {
it('should match state for invalid email (less than 2 characters), on form submission', () => {
store.dispatch = jest.fn(store.dispatch);
render(reduxWrapper(<IntlLoginPage {...props} />));
render(reduxWrapper(<LoginPage {...props} />));
fireEvent.change(screen.getByText(
'',
@@ -162,7 +168,7 @@ describe('LoginPage', () => {
});
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(
'',
{ selector: '.btn-brand' },
@@ -176,7 +182,7 @@ describe('LoginPage', () => {
});
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(
'',
@@ -195,7 +201,7 @@ describe('LoginPage', () => {
it('should reset field related error messages on onFocus event', async () => {
store.dispatch = jest.fn(store.dispatch);
render(reduxWrapper(<IntlLoginPage {...props} />));
render(reduxWrapper(<LoginPage {...props} />));
await act(async () => {
// clicking submit button with empty fields to make the errors appear
@@ -224,7 +230,7 @@ describe('LoginPage', () => {
// ******** test form buttons and links ********
it('should match default button state', () => {
render(reduxWrapper(<IntlLoginPage {...props} />));
render(reduxWrapper(<LoginPage {...props} />));
expect(screen.getByText('Sign in')).toBeDefined();
});
@@ -237,7 +243,7 @@ describe('LoginPage', () => {
},
});
render(reduxWrapper(<IntlLoginPage {...props} />));
render(reduxWrapper(<LoginPage {...props} />));
expect(screen.getByText(
'pending',
@@ -245,7 +251,7 @@ describe('LoginPage', () => {
});
it('should show forgot password link', () => {
render(reduxWrapper(<IntlLoginPage {...props} />));
render(reduxWrapper(<LoginPage {...props} />));
expect(screen.getByText(
'Forgot password',
@@ -265,7 +271,7 @@ describe('LoginPage', () => {
},
});
render(reduxWrapper(<IntlLoginPage {...props} />));
render(reduxWrapper(<LoginPage {...props} />));
expect(screen.getByText(
'',
{ selector: `#${ssoProvider.id}` },
@@ -287,7 +293,7 @@ describe('LoginPage', () => {
},
});
const { queryByText } = render(reduxWrapper(<IntlLoginPage {...props} />));
const { queryByText } = render(reduxWrapper(<LoginPage {...props} />));
expect(queryByText('Company or school credentials')).toBeNull();
expect(queryByText('Or sign in with:')).toBeNull();
expect(queryByText('Institution/campus credentials')).toBeNull();
@@ -307,7 +313,7 @@ describe('LoginPage', () => {
},
});
const { queryByText } = render(reduxWrapper(<IntlLoginPage {...props} />));
const { queryByText } = render(reduxWrapper(<LoginPage {...props} />));
expect(queryByText('Company or school credentials')).toBeNull();
expect(queryByText('Or sign in with:')).toBeNull();
});
@@ -327,7 +333,7 @@ describe('LoginPage', () => {
},
});
const { queryByText } = render(reduxWrapper(<IntlLoginPage {...props} />));
const { queryByText } = render(reduxWrapper(<LoginPage {...props} />));
expect(queryByText('Or sign in with:')).toBeDefined();
expect(queryByText('Company or school credentials')).toBeDefined();
expect(queryByText('Institution/campus credentials')).toBeDefined();
@@ -352,7 +358,7 @@ describe('LoginPage', () => {
},
});
const { queryByText } = render(reduxWrapper(<IntlLoginPage {...props} />));
const { queryByText } = render(reduxWrapper(<LoginPage {...props} />));
expect(queryByText('Or sign in with:')).toBeDefined();
expect(queryByText('Company or school credentials')).toBeNull();
expect(queryByText('Institution/campus credentials')).toBeDefined();
@@ -380,7 +386,7 @@ describe('LoginPage', () => {
},
});
const { queryByText } = render(reduxWrapper(<IntlLoginPage {...props} />));
const { queryByText } = render(reduxWrapper(<LoginPage {...props} />));
expect(queryByText('Or sign in with:')).toBeDefined();
expect(queryByText('Institution/campus credentials')).toBeDefined();
@@ -400,7 +406,7 @@ describe('LoginPage', () => {
},
});
const { queryByText } = render(reduxWrapper(<IntlLoginPage {...props} />));
const { queryByText } = render(reduxWrapper(<LoginPage {...props} />));
expect(queryByText('Or sign in with:')).toBeNull();
expect(queryByText('Institution/campus credentials')).toBeNull();
expect(queryByText('Company or school credentials')).toBeNull();
@@ -418,7 +424,7 @@ describe('LoginPage', () => {
},
});
const { queryByText } = render(reduxWrapper(<IntlLoginPage {...props} />));
const { queryByText } = render(reduxWrapper(<LoginPage {...props} />));
expect(queryByText('Or sign in with:')).toBeDefined();
expect(queryByText('Company or school credentials')).toBeNull();
expect(queryByText('Institution/campus credentials')).toBeDefined();
@@ -441,7 +447,7 @@ describe('LoginPage', () => {
},
});
render(reduxWrapper(<IntlLoginPage {...props} />));
render(reduxWrapper(<LoginPage {...props} />));
expect(screen.getByText(
'',
{ selector: '#login-failure-alert' },
@@ -465,7 +471,7 @@ describe('LoginPage', () => {
+ 'linked '}${ getConfig().SITE_NAME } account. To link your accounts, sign in now using your ${
getConfig().SITE_NAME } password.`;
render(reduxWrapper(<IntlLoginPage {...props} />));
render(reduxWrapper(<LoginPage {...props} />));
expect(screen.getByText(
'',
{ selector: '#tpa-alert' },
@@ -484,7 +490,7 @@ describe('LoginPage', () => {
},
},
});
render(reduxWrapper(<IntlLoginPage {...props} />));
render(reduxWrapper(<LoginPage {...props} />));
expect(screen.getByText(
'',
{ selector: '#login-failure-alert' },
@@ -501,7 +507,7 @@ describe('LoginPage', () => {
},
});
render(reduxWrapper(<IntlLoginPage {...props} />));
render(reduxWrapper(<LoginPage {...props} />));
expect(screen.getByText(
'',
{ selector: '#login-failure-alert' },
@@ -525,7 +531,7 @@ describe('LoginPage', () => {
delete window.location;
window.location = { href: getConfig().BASE_URL };
render(reduxWrapper(<IntlLoginPage {...props} />));
render(reduxWrapper(<LoginPage {...props} />));
expect(window.location.href).toBe(dashboardURL);
});
@@ -552,7 +558,7 @@ describe('LoginPage', () => {
delete window.location;
window.location = { href: getConfig().BASE_URL };
render(reduxWrapper(<IntlLoginPage {...props} />));
render(reduxWrapper(<LoginPage {...props} />));
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + authCompleteUrl);
});
@@ -571,7 +577,7 @@ describe('LoginPage', () => {
delete window.location;
window.location = { href: getConfig().BASE_URL };
render(reduxWrapper(<IntlLoginPage {...props} />));
render(reduxWrapper(<LoginPage {...props} />));
fireEvent.click(screen.getByText(
'',
@@ -600,7 +606,7 @@ describe('LoginPage', () => {
delete window.location;
window.location = { href: getConfig().BASE_URL };
render(reduxWrapper(<IntlLoginPage {...props} />));
render(reduxWrapper(<LoginPage {...props} />));
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + finishAuthUrl);
});
@@ -622,7 +628,7 @@ describe('LoginPage', () => {
delete window.location;
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(
'',
{ selector: `#${ssoProvider.id}` },
@@ -649,7 +655,7 @@ describe('LoginPage', () => {
delete window.location;
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();
});
@@ -671,7 +677,7 @@ describe('LoginPage', () => {
window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: `?next=/dashboard&tpa_hint=${secondaryProviders.id}` };
secondaryProviders.iconImage = null;
render(reduxWrapper(<IntlLoginPage {...props} />));
render(reduxWrapper(<LoginPage {...props} />));
expect(window.location.href).toEqual(getConfig().LMS_BASE_URL + secondaryProviders.loginUrl);
});
@@ -691,7 +697,7 @@ describe('LoginPage', () => {
delete window.location;
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}`);
mergeConfig({
@@ -715,7 +721,7 @@ describe('LoginPage', () => {
delete window.location;
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(
'Show me other ways to sign in or register',
).textContent).toBeDefined();
@@ -741,7 +747,7 @@ describe('LoginPage', () => {
delete window.location;
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(
'Show me other ways to sign in',
).textContent).toBeDefined();
@@ -750,7 +756,7 @@ describe('LoginPage', () => {
// ******** miscellaneous tests ********
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');
});
@@ -764,7 +770,7 @@ describe('LoginPage', () => {
});
store.dispatch = jest.fn(store.dispatch);
render(reduxWrapper(<IntlLoginPage {...props} />));
render(reduxWrapper(<LoginPage {...props} />));
expect(store.dispatch).toHaveBeenCalledWith(backupLoginFormBegin(
{
formFields: {
@@ -778,7 +784,7 @@ describe('LoginPage', () => {
});
it('should send track event when forgot password link is clicked', () => {
render(reduxWrapper(<IntlLoginPage {...props} />));
render(reduxWrapper(<LoginPage {...props} />));
fireEvent.click(screen.getByText(
'Forgot password',
{ selector: '#forgot-password' },
@@ -797,7 +803,7 @@ describe('LoginPage', () => {
});
store.dispatch = jest.fn(store.dispatch);
render(reduxWrapper(<IntlLoginPage {...props} />));
render(reduxWrapper(<LoginPage {...props} />));
expect(store.dispatch).toHaveBeenCalledWith(backupLoginFormBegin(
{
formFields: {
@@ -826,7 +832,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#password').value).toEqual('test-password');
});

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react';
import { connect } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { getConfig } from '@edx/frontend-platform';
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
@@ -24,16 +24,20 @@ import { LOGIN_PAGE, REGISTER_PAGE } from '../data/constants';
import {
getTpaHint, getTpaProvider, updatePathWithQueryParams,
} from '../data/utils';
import { LoginPage } from '../login';
import { backupLoginForm } from '../login/data/actions';
import LoginComponentSlot from '../plugin-slots/LoginComponentSlot';
import { RegistrationPage } from '../register';
import { backupRegistrationForm } from '../register/data/actions';
const Logistration = (props) => {
const { selectedPage, tpaProviders } = props;
const Logistration = ({
selectedPage,
}) => {
const tpaHint = getTpaHint();
const tpaProviders = useSelector(tpaProvidersSelector);
const dispatch = useDispatch();
const {
providers, secondaryProviders,
providers,
secondaryProviders,
} = tpaProviders;
const { formatMessage } = useIntl();
const [institutionLogin, setInstitutionLogin] = useState(false);
@@ -45,7 +49,8 @@ const Logistration = (props) => {
useEffect(() => {
const authService = getAuthService();
if (authService) {
authService.getCsrfTokenService().getCsrfToken(getConfig().LMS_BASE_URL);
authService.getCsrfTokenService()
.getCsrfToken(getConfig().LMS_BASE_URL);
}
});
@@ -71,11 +76,11 @@ const Logistration = (props) => {
return;
}
sendTrackEvent(`edx.bi.${tabKey.replace('/', '')}_form.toggled`, { category: 'user-engagement' });
props.clearThirdPartyAuthContextErrorMessage();
dispatch(clearThirdPartyAuthContextErrorMessage());
if (tabKey === LOGIN_PAGE) {
props.backupRegistrationForm();
dispatch(backupRegistrationForm());
} else if (tabKey === REGISTER_PAGE) {
props.backupLoginForm();
dispatch(backupLoginForm());
}
setKey(tabKey);
};
@@ -111,7 +116,10 @@ const Logistration = (props) => {
{!institutionLogin && (
<h3 className="mb-4.5">{formatMessage(messages['logistration.sign.in'])}</h3>
)}
<LoginPage institutionLogin={institutionLogin} handleInstitutionLogin={handleInstitutionLogin} />
<LoginComponentSlot
institutionLogin={institutionLogin}
handleInstitutionLogin={handleInstitutionLogin}
/>
</div>
</>
)
@@ -124,12 +132,16 @@ const Logistration = (props) => {
</Tabs>
)
: (!isValidTpaHint() && !hideRegistrationLink && (
<Tabs defaultActiveKey={selectedPage} id="controlled-tab" onSelect={(tabKey) => handleOnSelect(tabKey, selectedPage)}>
<Tabs
defaultActiveKey={selectedPage}
id="controlled-tab"
onSelect={(tabKey) => handleOnSelect(tabKey, selectedPage)}
>
<Tab title={formatMessage(messages['logistration.register'])} eventKey={REGISTER_PAGE} />
<Tab title={formatMessage(messages['logistration.sign.in'])} eventKey={LOGIN_PAGE} />
</Tabs>
))}
{ key && (
{key && (
<Navigate to={updatePathWithQueryParams(key)} replace />
)}
<div id="main-content" className="main-content">
@@ -139,7 +151,12 @@ const Logistration = (props) => {
</h3>
)}
{selectedPage === LOGIN_PAGE
? <LoginPage institutionLogin={institutionLogin} handleInstitutionLogin={handleInstitutionLogin} />
? (
<LoginComponentSlot
institutionLogin={institutionLogin}
handleInstitutionLogin={handleInstitutionLogin}
/>
)
: (
<RegistrationPage
institutionLogin={institutionLogin}
@@ -156,35 +173,10 @@ const Logistration = (props) => {
Logistration.propTypes = {
selectedPage: PropTypes.string,
backupLoginForm: PropTypes.func.isRequired,
backupRegistrationForm: PropTypes.func.isRequired,
clearThirdPartyAuthContextErrorMessage: PropTypes.func.isRequired,
tpaProviders: PropTypes.shape({
providers: PropTypes.arrayOf(PropTypes.shape({})),
secondaryProviders: PropTypes.arrayOf(PropTypes.shape({})),
}),
};
Logistration.defaultProps = {
tpaProviders: {
providers: [],
secondaryProviders: [],
},
};
Logistration.defaultProps = {
selectedPage: REGISTER_PAGE,
};
const mapStateToProps = state => ({
tpaProviders: tpaProvidersSelector(state),
});
export default connect(
mapStateToProps,
{
backupLoginForm,
backupRegistrationForm,
clearThirdPartyAuthContextErrorMessage,
},
)(Logistration);
export default Logistration;

View File

@@ -1,9 +1,8 @@
import React from 'react';
import { Provider } from 'react-redux';
import { getConfig, mergeConfig } from '@edx/frontend-platform';
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 { MemoryRouter } from 'react-router-dom';
import configureStore from 'redux-mock-store';
@@ -23,7 +22,6 @@ jest.mock('@edx/frontend-platform/analytics', () => ({
jest.mock('@edx/frontend-platform/auth');
const mockStore = configureStore();
const IntlLogistration = injectIntl(Logistration);
describe('Logistration', () => {
let store = {};
@@ -50,16 +48,26 @@ describe('Logistration', () => {
marketingEmailsOptIn: true,
},
formFields: {
name: '', email: '', username: '', password: '',
name: '',
email: '',
username: '',
password: '',
},
emailSuggestion: {
suggestion: '', type: '',
suggestion: '',
type: '',
},
errors: {
name: '', email: '', username: '', password: '',
name: '',
email: '',
username: '',
password: '',
},
},
registrationResult: { success: false, redirectUrl: '' },
registrationResult: {
success: false,
redirectUrl: '',
},
registrationError: {},
usernameSuggestions: [],
validationApiRateLimited: false,
@@ -71,7 +79,18 @@ describe('Logistration', () => {
},
},
login: {
loginResult: { success: false, redirectUrl: '' },
loginResult: {
success: false,
redirectUrl: '',
},
loginFormData: {
formFields: {
emailOrUsername: '', password: '',
},
errors: {
emailOrUsername: '', password: '',
},
},
},
};
@@ -95,7 +114,7 @@ describe('Logistration', () => {
});
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
fireEvent.click(container.querySelector('a[data-rb-event-key="/register"]'));
@@ -107,14 +126,14 @@ describe('Logistration', () => {
ALLOW_PUBLIC_ACCOUNT_CREATION: true,
});
const { container } = render(reduxWrapper(<IntlLogistration />));
const { container } = render(reduxWrapper(<Logistration />));
expect(container.querySelector('RegistrationPage')).toBeDefined();
});
it('should render login page', () => {
const props = { selectedPage: LOGIN_PAGE };
const { container } = render(reduxWrapper(<IntlLogistration {...props} />));
const { container } = render(reduxWrapper(<Logistration {...props} />));
expect(container.querySelector('LoginPage')).toBeDefined();
});
@@ -125,7 +144,7 @@ describe('Logistration', () => {
});
let props = { selectedPage: LOGIN_PAGE };
const { rerender } = render(reduxWrapper(<IntlLogistration {...props} />));
const { rerender } = render(reduxWrapper(<Logistration {...props} />));
// verifying sign in heading
expect(screen.getByRole('heading', { level: 3 }).textContent).toEqual('Sign in');
@@ -133,7 +152,7 @@ describe('Logistration', () => {
// register page is still accessible when SHOW_REGISTRATION_LINKS is false
// but it needs to be accessed directly
props = { selectedPage: REGISTER_PAGE };
rerender(reduxWrapper(<IntlLogistration {...props} />));
rerender(reduxWrapper(<Logistration {...props} />));
// verifying register heading
expect(screen.getByRole('heading', { level: 3 }).textContent).toEqual('Register');
@@ -160,7 +179,7 @@ describe('Logistration', () => {
});
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
expect(screen.getByRole('heading', { level: 3 }).textContent).toEqual('Sign in');
@@ -190,7 +209,7 @@ describe('Logistration', () => {
});
const props = { selectedPage: LOGIN_PAGE };
render(reduxWrapper(<IntlLogistration {...props} />));
render(reduxWrapper(<Logistration {...props} />));
expect(screen.getByText('Institution/campus credentials')).toBeDefined();
// on clicking "Institution/campus credentials" button, it should display institution login page
@@ -221,7 +240,7 @@ describe('Logistration', () => {
});
const props = { selectedPage: LOGIN_PAGE };
render(reduxWrapper(<IntlLogistration {...props} />));
render(reduxWrapper(<Logistration {...props} />));
fireEvent.click(screen.getByText('Institution/campus credentials'));
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.institution_login_form.toggled', { category: 'user-engagement' });
@@ -253,7 +272,7 @@ describe('Logistration', () => {
delete window.location;
window.location = { hostname: getConfig().SITE_NAME, href: getConfig().BASE_URL };
render(reduxWrapper(<IntlLogistration />));
render(reduxWrapper(<Logistration />));
fireEvent.click(screen.getByText('Institution/campus credentials'));
expect(screen.getByText('Test University')).toBeDefined();
@@ -264,7 +283,7 @@ describe('Logistration', () => {
it('should fire action to backup registration form on tab click', () => {
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"]'));
expect(store.dispatch).toHaveBeenCalledWith(backupRegistrationForm());
});
@@ -272,14 +291,14 @@ describe('Logistration', () => {
it('should fire action to backup login form on tab click', () => {
store.dispatch = jest.fn(store.dispatch);
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"]'));
expect(store.dispatch).toHaveBeenCalledWith(backupLoginForm());
});
it('should clear tpa context errorMessage tab click', () => {
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"]'));
expect(store.dispatch).toHaveBeenCalledWith(clearThirdPartyAuthContextErrorMessage());
});

View File

@@ -0,0 +1,47 @@
# Login Component Slot
### Slot ID: `org.openedx.frontend.authn.login_component.v1`
## Description
This slot is used to replace/modify/hide the login component.
## Example
### Default content
![Default Login Page](./default_component.png)
### With a prepended message
![Login Page with ](./component_with_prefix.png)
The following `env.config.jsx` will add a message before the login component.
```js
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
// Load environment variables from .env file
const config = {
...process.env,
pluginSlots: {
'org.openedx.frontend.authn.login_component.v1': {
keepDefault: true,
plugins: [
{
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: 'test_plugin',
type: DIRECT_PLUGIN,
priority: 1,
RenderWidget: () => (
<h2>You're logging into TEST Instance.</h2>
)
},
},
],
},
},
};
export default config;
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

View File

@@ -0,0 +1,31 @@
import React from 'react';
import { PluginSlot } from '@openedx/frontend-plugin-framework';
import PropTypes from 'prop-types';
import LoginPage from '../../login/LoginPage';
const LoginComponentSlot = ({
institutionLogin,
handleInstitutionLogin,
}) => (
<PluginSlot
id="org.openedx.frontend.authn.login_component.v1"
pluginProps={{
isInstitutionLogin: institutionLogin,
setInstitutionLogin: handleInstitutionLogin,
}}
>
<LoginPage
institutionLogin={institutionLogin}
handleInstitutionLogin={handleInstitutionLogin}
/>
</PluginSlot>
);
LoginComponentSlot.propTypes = {
institutionLogin: PropTypes.bool,
handleInstitutionLogin: PropTypes.func,
};
export default LoginComponentSlot;

View File

@@ -1,10 +1,9 @@
import React from 'react';
import { Provider } from 'react-redux';
import { getConfig, mergeConfig } from '@edx/frontend-platform';
import { identifyAuthenticatedUser, sendTrackEvent } from '@edx/frontend-platform/analytics';
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 {
fireEvent, render, screen,
} from '@testing-library/react';
@@ -22,7 +21,6 @@ import {
import { saveUserProfile } from '../data/actions';
import ProgressiveProfiling from '../ProgressiveProfiling';
const IntlProgressiveProfilingPage = injectIntl(ProgressiveProfiling);
const mockStore = configureStore();
jest.mock('@edx/frontend-platform/analytics', () => ({
@@ -114,7 +112,7 @@ describe('ProgressiveProfilingTests', () => {
mergeConfig({
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 });
expect(button).toBeNull();
@@ -125,7 +123,7 @@ describe('ProgressiveProfilingTests', () => {
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.');
@@ -135,7 +133,7 @@ describe('ProgressiveProfilingTests', () => {
it('should open modal on pressing skip for now button', () => {
delete window.location;
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 });
fireEvent.click(skipButton);
@@ -150,7 +148,7 @@ describe('ProgressiveProfilingTests', () => {
// ******** test event functionality ********
it('should make identify call to segment on progressive profiling page', () => {
render(reduxWrapper(<IntlProgressiveProfilingPage />));
render(reduxWrapper(<ProgressiveProfiling />));
expect(identifyAuthenticatedUser).toHaveBeenCalledWith(3);
expect(identifyAuthenticatedUser).toHaveBeenCalled();
@@ -160,7 +158,7 @@ describe('ProgressiveProfilingTests', () => {
mergeConfig({
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 });
fireEvent.click(supportLink);
@@ -178,7 +176,7 @@ describe('ProgressiveProfilingTests', () => {
};
delete window.location;
window.location = { href: getConfig().BASE_URL.concat(AUTHN_PROGRESSIVE_PROFILING) };
render(reduxWrapper(<IntlProgressiveProfilingPage />));
render(reduxWrapper(<ProgressiveProfiling />));
const nextButton = screen.getByText('Next');
fireEvent.click(nextButton);
@@ -194,7 +192,7 @@ describe('ProgressiveProfilingTests', () => {
extended_profile: [{ field_name: 'company', field_value: 'test company' }],
};
store.dispatch = jest.fn(store.dispatch);
const { getByLabelText, getByText } = render(reduxWrapper(<IntlProgressiveProfilingPage />));
const { getByLabelText, getByText } = render(reduxWrapper(<ProgressiveProfiling />));
const genderSelect = getByLabelText('Gender');
const companyInput = getByLabelText('Company');
@@ -216,7 +214,7 @@ describe('ProgressiveProfilingTests', () => {
},
});
const { container } = render(reduxWrapper(<IntlProgressiveProfilingPage />));
const { container } = render(reduxWrapper(<ProgressiveProfiling />));
const errorElement = container.querySelector('#pp-page-errors');
expect(errorElement).toBeTruthy();
@@ -232,7 +230,7 @@ describe('ProgressiveProfilingTests', () => {
href: getConfig().BASE_URL,
};
render(reduxWrapper(<IntlProgressiveProfilingPage />));
render(reduxWrapper(<ProgressiveProfiling />));
expect(window.location.href).toEqual(DASHBOARD_URL);
});
@@ -250,7 +248,7 @@ describe('ProgressiveProfilingTests', () => {
success: true,
},
});
const { container } = render(reduxWrapper(<IntlProgressiveProfilingPage />));
const { container } = render(reduxWrapper(<ProgressiveProfiling />));
const nextButton = container.querySelector('button.btn-brand');
expect(nextButton.textContent).toEqual('Next');
@@ -277,7 +275,7 @@ describe('ProgressiveProfilingTests', () => {
},
});
const { container } = render(reduxWrapper(<IntlProgressiveProfilingPage />));
const { container } = render(reduxWrapper(<ProgressiveProfiling />));
const nextButton = container.querySelector('button.btn-brand');
expect(nextButton.textContent).toEqual('Submit');
@@ -311,7 +309,7 @@ describe('ProgressiveProfilingTests', () => {
href: getConfig().BASE_URL.concat(AUTHN_PROGRESSIVE_PROFILING),
search: `?host=${host}&variant=${EMBEDDED}`,
};
render(reduxWrapper(<IntlProgressiveProfilingPage />));
render(reduxWrapper(<ProgressiveProfiling />));
const skipLinkButton = screen.getByText('Skip for now');
fireEvent.click(skipLinkButton);
@@ -336,7 +334,7 @@ describe('ProgressiveProfilingTests', () => {
},
});
const { container } = render(reduxWrapper(<IntlProgressiveProfilingPage />));
const { container } = render(reduxWrapper(<ProgressiveProfiling />));
const tpaSpinnerElement = container.querySelector('#tpa-spinner');
expect(tpaSpinnerElement).toBeTruthy();
@@ -355,7 +353,7 @@ describe('ProgressiveProfilingTests', () => {
href: getConfig().BASE_URL.concat(AUTHN_PROGRESSIVE_PROFILING),
search: `?host=${host}`,
};
render(reduxWrapper(<IntlProgressiveProfilingPage />));
render(reduxWrapper(<ProgressiveProfiling />));
const submitButton = screen.getByText('Next');
fireEvent.click(submitButton);
@@ -370,7 +368,7 @@ describe('ProgressiveProfilingTests', () => {
search: `?variant=${EMBEDDED}&host=${host}`,
};
const { container } = render(reduxWrapper(<IntlProgressiveProfilingPage />));
const { container } = render(reduxWrapper(<ProgressiveProfiling />));
const genderField = container.querySelector('#gender');
expect(genderField).toBeTruthy();
@@ -391,7 +389,7 @@ describe('ProgressiveProfilingTests', () => {
},
});
render(reduxWrapper(<IntlProgressiveProfilingPage />));
render(reduxWrapper(<ProgressiveProfiling />));
expect(window.location.href).toBe(DASHBOARD_URL);
});
@@ -419,7 +417,7 @@ describe('ProgressiveProfilingTests', () => {
},
});
render(reduxWrapper(<IntlProgressiveProfilingPage />));
render(reduxWrapper(<ProgressiveProfiling />));
const submitButton = screen.getByText('Submit');
fireEvent.click(submitButton);
expect(window.location.href).toBe(redirectUrl);

View File

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

View File

@@ -1,10 +1,12 @@
import { renderHook } from '@testing-library/react-hooks';
import React from 'react';
import { render } from '@testing-library/react';
import algoliasearchHelper from 'algoliasearch-helper';
import mockedRecommendedProducts from './mockedData';
import CreateAlgoliaSearchHelperMock from './test_utils/test_utils';
import isOneTrustFunctionalCookieEnabled from '../../../data/oneTrust';
import useAlgoliaRecommendations from '../hooks/useAlgoliaRecommendations';
import CreateAlgoliaSearchHelperMock from './test_utils/test_utils';
jest.mock('algoliasearch-helper');
@@ -17,6 +19,23 @@ jest.mock('../../../data/algolia', () => ({
jest.mock('../algoliaResultsParser', () => jest.fn((course) => course));
const renderHook = (hookCallback) => {
const result = {
current: null,
};
const Component = () => {
const val = hookCallback();
React.useEffect(() => {
result.current = val;
});
return null;
};
render(<Component />);
return { result };
};
describe('useAlgoliaRecommendations Tests', () => {
const MockSearchHelperWithData = new CreateAlgoliaSearchHelperMock(mockedRecommendedProducts);
const MockSearchHelperWithoutData = new CreateAlgoliaSearchHelperMock();
@@ -28,8 +47,10 @@ describe('useAlgoliaRecommendations Tests', () => {
() => useAlgoliaRecommendations('PK', 'Introductory'),
);
expect(result.current.recommendations).toEqual(mockedRecommendedProducts);
expect(result.current.isLoading).toBe(false);
expect(result.current.recommendations)
.toEqual(mockedRecommendedProducts);
expect(result.current.isLoading)
.toBe(false);
});
it('should not fetch recommendations if functional cookies are not set', async () => {
@@ -39,8 +60,10 @@ describe('useAlgoliaRecommendations Tests', () => {
() => useAlgoliaRecommendations('PK', 'Introductory'),
);
expect(result.current.recommendations).toEqual([]);
expect(result.current.isLoading).toBe(false);
expect(result.current.recommendations)
.toEqual([]);
expect(result.current.isLoading)
.toBe(false);
});
it('should return empty list if no recommendations returned from Algolia', async () => {
@@ -50,7 +73,9 @@ describe('useAlgoliaRecommendations Tests', () => {
() => useAlgoliaRecommendations('PK', 'Introductory'),
);
expect(result.current.recommendations).toEqual([]);
expect(result.current.isLoading).toBe(false);
expect(result.current.recommendations)
.toEqual([]);
expect(result.current.isLoading)
.toBe(false);
});
});

View File

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

View File

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

View File

@@ -97,7 +97,7 @@ const CountryField = (props) => {
};
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]}
</FormAutosuggestOption>
));

View File

@@ -1,8 +1,7 @@
import React from 'react';
import { Provider } from 'react-redux';
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 { BrowserRouter as Router } from 'react-router-dom';
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 { CountryField } from '../index';
const IntlCountryField = injectIntl(CountryField);
const mockStore = configureStore();
jest.mock('react-router-dom', () => {
@@ -82,7 +80,7 @@ describe('CountryField', () => {
};
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"]');
fireEvent.blur(countryInput, {
@@ -97,7 +95,7 @@ describe('CountryField', () => {
});
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"]');
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', () => {
const { container } = render(routerWrapper(reduxWrapper(<IntlCountryField {...props} />)));
const { container } = render(routerWrapper(reduxWrapper(<CountryField {...props} />)));
const countryInput = container.querySelector('input[name="country"]');
const dropdownArrowIcon = container.querySelector('.btn-icon.pgn__form-autosuggest__icon-button');
@@ -125,7 +123,7 @@ describe('CountryField', () => {
});
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"]');
fireEvent.blur(countryInput, { target: { value: '', name: 'country' } });
@@ -135,7 +133,7 @@ describe('CountryField', () => {
});
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"]');
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"]');
expect(props.onChangeHandler).toHaveBeenCalledTimes(1);
@@ -164,7 +162,7 @@ describe('CountryField', () => {
});
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');
fireEvent.click(dropdownButton);
@@ -181,7 +179,7 @@ describe('CountryField', () => {
it('should set value on change', () => {
const { container } = render(
routerWrapper(reduxWrapper(<IntlCountryField {...props} />)),
routerWrapper(reduxWrapper(<CountryField {...props} />)),
);
const countryInput = container.querySelector('input[name="country"]');
@@ -200,7 +198,7 @@ describe('CountryField', () => {
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"]');
expect(feedbackElement).toBeTruthy();

View File

@@ -1,8 +1,7 @@
import React from 'react';
import { Provider } from 'react-redux';
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 { BrowserRouter as Router } from 'react-router-dom';
import configureStore from 'redux-mock-store';
@@ -10,7 +9,6 @@ import configureStore from 'redux-mock-store';
import { clearRegistrationBackendError, fetchRealtimeValidations } from '../../data/actions';
import { EmailField } from '../index';
const IntlEmailField = injectIntl(EmailField);
const mockStore = configureStore();
jest.mock('react-router-dom', () => {
@@ -80,7 +78,7 @@ describe('EmailField', () => {
};
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');
fireEvent.blur(emailInput, { target: { value: '', name: 'email' } });
@@ -92,7 +90,7 @@ describe('EmailField', () => {
});
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');
fireEvent.blur(emailInput, { target: { value: 'ab', name: 'email' } });
@@ -105,7 +103,7 @@ describe('EmailField', () => {
});
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');
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', () => {
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
const emailInput = container.querySelector('input#email');
@@ -129,7 +127,7 @@ describe('EmailField', () => {
});
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');
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', () => {
const { container } = render(routerWrapper(reduxWrapper(<IntlEmailField {...props} />)));
const { container } = render(routerWrapper(reduxWrapper(<EmailField {...props} />)));
const emailInput = container.querySelector('input#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', () => {
const { container } = render(routerWrapper(reduxWrapper(<IntlEmailField {...props} />)));
const { container } = render(routerWrapper(reduxWrapper(<EmailField {...props} />)));
const emailInput = container.querySelector('input#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', () => {
const { container } = render(routerWrapper(reduxWrapper(<IntlEmailField {...props} />)));
const { container } = render(routerWrapper(reduxWrapper(<EmailField {...props} />)));
const emailInput = container.querySelector('input#email');
fireEvent.blur(emailInput, { target: { value: 'john@gmail', name: 'email' } });
@@ -194,7 +192,7 @@ describe('EmailField', () => {
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');
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', () => {
const { container } = render(routerWrapper(reduxWrapper(<IntlEmailField {...props} />)));
const { container } = render(routerWrapper(reduxWrapper(<EmailField {...props} />)));
const emailInput = container.querySelector('input#email');
fireEvent.blur(emailInput, { target: { value: 'john@gmail.mistake', name: 'email' } });
@@ -224,7 +222,7 @@ describe('EmailField', () => {
confirmEmailValue: 'confirmEmail@yopmail.com',
};
const { container } = render(routerWrapper(reduxWrapper(<IntlEmailField {...props} />)));
const { container } = render(routerWrapper(reduxWrapper(<EmailField {...props} />)));
const emailInput = container.querySelector('input#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 { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { render } from '@testing-library/react';
import { HonorCode } from '../index';
const IntlHonorCode = injectIntl(HonorCode);
describe('HonorCodeTest', () => {
mergeConfig({
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 { container } = render(
<IntlProvider locale="en">
<IntlHonorCode
<HonorCode
errorMessage={errorMessage}
onChangeHandler={changeHandler}
/>
@@ -43,7 +39,7 @@ describe('HonorCodeTest', () => {
const expectedMsg = 'I agree to the Your Platform Name Here\u00a0Honor Codein a new tab';
const { container } = render(
<IntlProvider locale="en">
<IntlHonorCode onChangeHandler={changeHandler} />
<HonorCode onChangeHandler={changeHandler} />
</IntlProvider>,
);
@@ -56,7 +52,7 @@ describe('HonorCodeTest', () => {
it('should render Terms of Service and Honor code field', () => {
const { container } = render(
<IntlProvider locale="en">
<IntlHonorCode fieldType="tos_and_honor_code" onChangeHandler={changeHandler} />
<HonorCode fieldType="tos_and_honor_code" onChangeHandler={changeHandler} />
</IntlProvider>,
);
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 { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { fireEvent, render } from '@testing-library/react';
import { BrowserRouter as Router } from 'react-router-dom';
import configureStore from 'redux-mock-store';
@@ -9,7 +8,6 @@ import configureStore from 'redux-mock-store';
import { clearRegistrationBackendError, fetchRealtimeValidations } from '../../data/actions';
import { NameField } from '../index';
const IntlNameField = injectIntl(NameField);
const mockStore = configureStore();
jest.mock('react-router-dom', () => {
@@ -69,7 +67,7 @@ describe('NameField', () => {
const fieldValidation = { name: 'Enter your full name' };
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');
fireEvent.blur(nameInput, { target: { value: '', name: 'name' } });
@@ -82,7 +80,7 @@ describe('NameField', () => {
});
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');
fireEvent.blur(nameInput, { target: { value: 'https://invalid-name.com', name: 'name' } });
@@ -95,7 +93,7 @@ describe('NameField', () => {
});
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');
fireEvent.focus(nameInput, { target: { value: '', name: 'name' } });
@@ -113,7 +111,7 @@ describe('NameField', () => {
...props,
shouldFetchUsernameSuggestions: true,
};
const { container } = render(routerWrapper(reduxWrapper(<IntlNameField {...props} />)));
const { container } = render(routerWrapper(reduxWrapper(<NameField {...props} />)));
const nameInput = container.querySelector('input#name');
// Enter a valid name so that frontend validations are passed
@@ -135,7 +133,7 @@ describe('NameField', () => {
});
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');

View File

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

View File

@@ -101,7 +101,7 @@ const UsernameField = (props) => {
};
const suggestedUsernames = () => (
<div className={className}>
<div className={className} role="listbox">
<span className="text-gray username-suggestion--label">{formatMessage(messages['registration.username.suggestion.label'])}</span>
<div className="username-scroll-suggested--form-field">
{usernameSuggestions.map((username, index) => (
@@ -112,7 +112,9 @@ const UsernameField = (props) => {
className="username-suggestions--chip data-hj-suppress"
autoComplete={props.autoComplete}
key={`suggestion-${index.toString()}`}
tabIndex={0}
onClick={(e) => handleSuggestionClick(e, username)}
role="option"
>
{username}
</Button>
@@ -123,7 +125,7 @@ const UsernameField = (props) => {
);
if (usernameSuggestions.length > 0 && errorMessage && value === ' ') {
className = 'username-suggestions__error';
className = 'username-suggestions';
iconButton = <IconButton src={Close} iconAs={Icon} alt="Close" onClick={() => handleUsernameSuggestionClose()} variant="black" size="sm" className="username-suggestions__close__button" />;
suggestedUsernameDiv = suggestedUsernames();
} else if (usernameSuggestions.length > 0 && value === ' ') {
@@ -134,14 +136,15 @@ const UsernameField = (props) => {
suggestedUsernameDiv = suggestedUsernames();
}
return (
<FormGroup
{...props}
handleChange={handleOnChange}
handleFocus={handleOnFocus}
handleBlur={handleOnBlur}
>
<div className="username__form-group-wrapper">
{suggestedUsernameDiv}
</FormGroup>
<FormGroup
{...props}
handleChange={handleOnChange}
handleFocus={handleOnFocus}
handleBlur={handleOnBlur}
/>
</div>
);
};

View File

@@ -1,7 +1,6 @@
import React from 'react';
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 { BrowserRouter as Router } from 'react-router-dom';
import configureStore from 'redux-mock-store';
@@ -9,7 +8,6 @@ import configureStore from 'redux-mock-store';
import { clearRegistrationBackendError, clearUsernameSuggestions, fetchRealtimeValidations } from '../../data/actions';
import { UsernameField } from '../index';
const IntlUsernameField = injectIntl(UsernameField);
const mockStore = configureStore();
jest.mock('react-router-dom', () => {
@@ -73,7 +71,7 @@ describe('UsernameField', () => {
};
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');
fireEvent.blur(usernameField, { target: { value: '', name: 'username' } });
@@ -86,7 +84,7 @@ describe('UsernameField', () => {
});
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');
fireEvent.blur(usernameField, { target: { value: 'user#', name: 'username' } });
@@ -99,7 +97,7 @@ describe('UsernameField', () => {
});
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');
fireEvent.focus(usernameField, { target: { value: '', name: 'username' } });
@@ -112,7 +110,7 @@ describe('UsernameField', () => {
});
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');
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', () => {
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');
// 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', () => {
const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
const { container } = render(routerWrapper(reduxWrapper(<UsernameField {...props} />)));
const usernameField = container.querySelector('input#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', () => {
const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
const { container } = render(routerWrapper(reduxWrapper(<UsernameField {...props} />)));
const usernameField = container.querySelector('input#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', () => {
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');
fireEvent.focus(usernameField);
@@ -179,7 +177,7 @@ describe('UsernameField', () => {
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');
expect(usernameSuggestions.length).toEqual(3);
});
@@ -198,7 +196,7 @@ describe('UsernameField', () => {
value: ' ',
};
const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
const { container } = render(routerWrapper(reduxWrapper(<UsernameField {...props} />)));
const usernameSuggestions = container.querySelectorAll('button.username-suggestions--chip');
expect(usernameSuggestions.length).toEqual(3);
});
@@ -218,7 +216,7 @@ describe('UsernameField', () => {
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');
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).toHaveBeenCalledWith(
{ target: { name: 'username', value: ' ' } },
@@ -253,7 +251,7 @@ describe('UsernameField', () => {
value: ' ',
};
const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
const { container } = render(routerWrapper(reduxWrapper(<UsernameField {...props} />)));
const usernameSuggestion = container.querySelector('.username-suggestions--chip');
fireEvent.click(usernameSuggestion);
expect(props.handleChange).toHaveBeenCalledTimes(1);
@@ -277,7 +275,7 @@ describe('UsernameField', () => {
value: ' ',
};
const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
const { container } = render(routerWrapper(reduxWrapper(<UsernameField {...props} />)));
let closeButton = container.querySelector('button.username-suggestions__close__button');
fireEvent.click(closeButton);
expect(store.dispatch).toHaveBeenCalledWith(clearUsernameSuggestions());
@@ -287,7 +285,7 @@ describe('UsernameField', () => {
errorMessage: 'username error',
};
render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
render(routerWrapper(reduxWrapper(<UsernameField {...props} />)));
closeButton = container.querySelector('button.username-suggestions__close__button');
fireEvent.click(closeButton);
expect(store.dispatch).toHaveBeenCalledWith(clearUsernameSuggestions());
@@ -309,7 +307,7 @@ describe('UsernameField', () => {
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');
fireEvent.focus(usernameField, { target: { value: 'test', name: 'username' } });

View File

@@ -60,6 +60,7 @@ const RegistrationPage = (props) => {
showConfigurableEdxFields: getConfig().SHOW_CONFIGURABLE_EDX_FIELDS,
showConfigurableRegistrationFields: getConfig().ENABLE_DYNAMIC_REGISTRATION_FIELDS,
showMarketingEmailOptInCheckbox: getConfig().MARKETING_EMAILS_OPT_IN,
autoGeneratedUsernameEnabled: getConfig().ENABLE_AUTO_GENERATED_USERNAME,
};
const {
handleInstitutionLogin,
@@ -215,6 +216,9 @@ const RegistrationPage = (props) => {
delete payload.password;
payload.social_auth_provider = currentProvider;
}
if (flags.autoGeneratedUsernameEnabled) {
delete payload.username;
}
// Validating form data before submitting
const { isValid, fieldErrors, emailSuggestion } = isFormValid(
@@ -324,16 +328,18 @@ const RegistrationPage = (props) => {
helpText={[formatMessage(messages['help.text.email'])]}
floatingLabel={formatMessage(messages['registration.email.label'])}
/>
<UsernameField
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'])}
/>
{!flags.autoGeneratedUsernameEnabled && (
<UsernameField
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"

View File

@@ -1,10 +1,9 @@
import React from 'react';
import { Provider } from 'react-redux';
import { getConfig, mergeConfig } from '@edx/frontend-platform';
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
import {
configure, getLocale, injectIntl, IntlProvider,
configure, getLocale, IntlProvider,
} from '@edx/frontend-platform/i18n';
import { fireEvent, render } from '@testing-library/react';
import { mockNavigate, BrowserRouter as Router } from 'react-router-dom';
@@ -31,7 +30,6 @@ jest.mock('@edx/frontend-platform/i18n', () => ({
getLocale: jest.fn(),
}));
const IntlRegistrationPage = injectIntl(RegistrationPage);
const mockStore = configureStore();
jest.mock('react-router-dom', () => {
@@ -134,9 +132,16 @@ describe('RegistrationPage', () => {
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('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('Country/Region'), { target: { value: payload.country, name: 'country' } });
@@ -176,12 +181,12 @@ describe('RegistrationPage', () => {
password: 'password1',
country: 'Pakistan',
honor_code: true,
totalRegistrationTime: 0,
total_registration_time: 0,
next: '/course/demo-course-url',
};
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);
const button = container.querySelector('button.btn-brand');
fireEvent.click(button);
@@ -199,7 +204,7 @@ describe('RegistrationPage', () => {
country: 'Pakistan',
honor_code: true,
social_auth_provider: 'Apple',
totalRegistrationTime: 0,
total_registration_time: 0,
};
store = mockStore({
@@ -213,7 +218,7 @@ describe('RegistrationPage', () => {
},
});
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);
const button = container.querySelector('button.btn-brand');
@@ -232,11 +237,11 @@ describe('RegistrationPage', () => {
password: 'password1',
country: 'Ukraine',
honor_code: true,
totalRegistrationTime: 0,
total_registration_time: 0,
};
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);
const button = container.querySelector('button.btn-brand');
@@ -257,11 +262,11 @@ describe('RegistrationPage', () => {
password: 'password1',
country: 'Ukraine',
honor_code: true,
totalRegistrationTime: 0,
total_registration_time: 0,
};
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);
const button = container.querySelector('button.btn-brand');
fireEvent.click(button);
@@ -283,12 +288,12 @@ describe('RegistrationPage', () => {
password: 'password1',
country: 'Pakistan',
honor_code: true,
totalRegistrationTime: 0,
total_registration_time: 0,
marketing_emails_opt_in: true,
};
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);
const button = container.querySelector('button.btn-brand');
fireEvent.click(button);
@@ -299,10 +304,48 @@ describe('RegistrationPage', () => {
});
});
it('should submit form without UsernameField when autoGeneratedUsernameEnabled is true', () => {
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,
};
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);
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', () => {
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');
fireEvent.click(button);
@@ -313,7 +356,7 @@ describe('RegistrationPage', () => {
// ******** test registration form validations ********
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');
fireEvent.click(button);
@@ -341,7 +384,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 emailFeedback = container.querySelector('div[feedback-for="email"]');
@@ -350,7 +393,7 @@ describe('RegistrationPage', () => {
});
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');
fireEvent.click(submitButton);
@@ -379,7 +422,7 @@ describe('RegistrationPage', () => {
store.dispatch = jest.fn(store.dispatch);
const { container } = render(routerWrapper(reduxWrapper(
<IntlRegistrationPage {...props} />,
<RegistrationPage {...props} />,
)));
const emailInput = container.querySelector('input#email');
@@ -390,7 +433,7 @@ describe('RegistrationPage', () => {
// ******** test form buttons and fields ********
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');
expect(button.textContent).toEqual('Create an account for free');
});
@@ -404,7 +447,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');
expect(button.textContent).toEqual('pending');
@@ -415,7 +458,7 @@ describe('RegistrationPage', () => {
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');
expect(checkboxDivs.length).toEqual(1);
@@ -428,7 +471,7 @@ describe('RegistrationPage', () => {
const buttonLabel = 'Register';
delete window.location;
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 buttonText = button.textContent;
@@ -447,7 +490,7 @@ describe('RegistrationPage', () => {
},
});
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
expect(document.cookie).toMatch(`${getConfig().USER_RETENTION_COOKIE_NAME}=true`);
});
@@ -465,7 +508,7 @@ describe('RegistrationPage', () => {
});
delete window.location;
window.location = { href: getConfig().BASE_URL };
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
expect(window.location.href).toBe(dashboardURL);
});
@@ -492,7 +535,7 @@ describe('RegistrationPage', () => {
});
delete window.location;
window.location = { href: getConfig().BASE_URL };
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
expect(window.location.href).toBe(dashboardUrl);
});
@@ -523,7 +566,7 @@ describe('RegistrationPage', () => {
render(reduxWrapper(
<Router>
<IntlRegistrationPage {...props} />
<RegistrationPage {...props} />
</Router>,
));
expect(mockNavigate).toHaveBeenCalledWith(AUTHN_PROGRESSIVE_PROFILING);
@@ -541,12 +584,12 @@ describe('RegistrationPage', () => {
});
store.dispatch = jest.fn(store.dispatch);
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
expect(store.dispatch).toHaveBeenCalledWith(backupRegistrationFormBegin({ ...registrationFormData }));
});
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');
});
@@ -564,7 +607,7 @@ describe('RegistrationPage', () => {
delete window.location;
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', {});
});
@@ -590,7 +633,7 @@ describe('RegistrationPage', () => {
store.dispatch = jest.fn(store.dispatch);
const { container } = render(reduxWrapper(
<Router>
<IntlRegistrationPage {...props} />
<RegistrationPage {...props} />
</Router>,
));
@@ -613,7 +656,7 @@ describe('RegistrationPage', () => {
},
});
const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
const { container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
const validationErrors = container.querySelector('div#validation-errors');
expect(validationErrors.textContent).toContain(
'An error has occurred. Try refreshing the page, or check your internet connection.',
@@ -640,7 +683,7 @@ describe('RegistrationPage', () => {
},
});
const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
const { container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
const fullNameInput = container.querySelector('input#name');
const usernameInput = container.querySelector('input#username');
@@ -686,14 +729,14 @@ describe('RegistrationPage', () => {
},
},
});
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
expect(window.parent.postMessage).toHaveBeenCalledTimes(2);
});
it('should not display validations error on blur event when embedded variant is rendered', () => {
delete window.location;
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');
fireEvent.blur(usernameInput, { target: { value: '', name: 'username' } });
@@ -721,7 +764,7 @@ describe('RegistrationPage', () => {
},
});
const { container } = render(routerWrapper(reduxWrapper(
<IntlRegistrationPage {...props} />),
<RegistrationPage {...props} />),
));
const usernameFeedback = container.querySelector('div[feedback-for="username"]');
@@ -738,7 +781,7 @@ describe('RegistrationPage', () => {
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');
fireEvent.click(submitButton);
@@ -779,7 +822,7 @@ describe('RegistrationPage', () => {
});
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 registrationFormElement = container.querySelector('#registration-form');
@@ -830,14 +873,14 @@ describe('RegistrationPage', () => {
});
store.dispatch = jest.fn(store.dispatch);
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({
name: 'John Doe',
username: 'john_doe',
email: 'john.doe@example.com',
country: 'PK',
social_auth_provider: 'Apple',
totalRegistrationTime: 0,
total_registration_time: 0,
}));
});
});

View File

@@ -33,7 +33,11 @@ const ConfigurableRegistrationForm = (props) => {
autoSubmitRegistrationForm,
} = props;
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.
*/
const countryList = useMemo(() => getCountryList(getLocale()).concat([{ code: 'US', name: 'United States' }]), []);
let showTermsOfServiceAndHonorCode = false;
let showCountryField = false;

View File

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

View File

@@ -1,9 +1,8 @@
import React from 'react';
import { Provider } from 'react-redux';
import { mergeConfig } from '@edx/frontend-platform';
import {
getLocale, injectIntl, IntlProvider,
getLocale, IntlProvider,
} from '@edx/frontend-platform/i18n';
import { fireEvent, render } from '@testing-library/react';
import { BrowserRouter as Router } from 'react-router-dom';
@@ -23,8 +22,6 @@ jest.mock('@edx/frontend-platform/i18n', () => ({
getLocale: jest.fn(),
}));
const IntlConfigurableRegistrationForm = injectIntl(ConfigurableRegistrationForm);
const IntlRegistrationPage = injectIntl(RegistrationPage);
const mockStore = configureStore();
jest.mock('react-router-dom', () => {
@@ -158,7 +155,7 @@ describe('ConfigurableRegistrationForm', () => {
};
render(routerWrapper(reduxWrapper(
<IntlConfigurableRegistrationForm {...props} />,
<ConfigurableRegistrationForm {...props} />,
)));
expect(document.querySelector('#profession')).toBeTruthy();
@@ -188,7 +185,7 @@ describe('ConfigurableRegistrationForm', () => {
};
render(routerWrapper(reduxWrapper(
<IntlConfigurableRegistrationForm {...props} />,
<ConfigurableRegistrationForm {...props} />,
)));
expect(props.setFormFields).toHaveBeenCalledTimes(2);
@@ -215,7 +212,7 @@ describe('ConfigurableRegistrationForm', () => {
},
},
});
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
expect(document.querySelector('#profession')).toBeTruthy();
expect(document.querySelector('#tos')).toBeTruthy();
});
@@ -245,11 +242,11 @@ describe('ConfigurableRegistrationForm', () => {
country: 'Pakistan',
honor_code: true,
profession: 'Engineer',
totalRegistrationTime: 0,
total_registration_time: 0,
};
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);
@@ -284,7 +281,7 @@ describe('ConfigurableRegistrationForm', () => {
},
});
const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
const { container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
const submitButton = container.querySelector('button.btn-brand');
fireEvent.click(submitButton);
@@ -310,7 +307,7 @@ describe('ConfigurableRegistrationForm', () => {
},
},
});
const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
const { container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
const countryInput = container.querySelector('input[name="country"]');
fireEvent.change(countryInput, { target: { value: 'Pak', name: 'country' } });
fireEvent.blur(countryInput, { target: { value: 'Pak', name: 'country' } });
@@ -335,7 +332,7 @@ describe('ConfigurableRegistrationForm', () => {
},
},
});
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
const emailInput = getByLabelText('Email');
const confirmEmailInput = getByLabelText('Confirm Email');
@@ -356,7 +353,7 @@ describe('ConfigurableRegistrationForm', () => {
password: 'password1',
country: 'Ukraine',
honor_code: true,
totalRegistrationTime: 0,
total_registration_time: 0,
};
store = mockStore({
@@ -371,7 +368,7 @@ describe('ConfigurableRegistrationForm', () => {
},
},
});
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
populateRequiredFields(getByLabelText, formPayload, true);
fireEvent.change(
@@ -406,7 +403,7 @@ describe('ConfigurableRegistrationForm', () => {
});
const { getByLabelText, container } = render(
routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)),
routerWrapper(reduxWrapper(<RegistrationPage {...props} />)),
);
const professionInput = getByLabelText('Profession');

View File

@@ -1,9 +1,8 @@
import React from 'react';
import { Provider } from 'react-redux';
import { mergeConfig } from '@edx/frontend-platform';
import {
configure, getLocale, injectIntl, IntlProvider,
configure, getLocale, IntlProvider,
} from '@edx/frontend-platform/i18n';
import { render, screen } from '@testing-library/react';
import { BrowserRouter as Router } from 'react-router-dom';
@@ -24,8 +23,6 @@ jest.mock('@edx/frontend-platform/i18n', () => ({
getLocale: jest.fn(),
}));
const IntlRegistrationPage = injectIntl(RegistrationPage);
const IntlRegistrationFailure = injectIntl(RegistrationFailureMessage);
const mockStore = configureStore();
jest.mock('react-router-dom', () => {
@@ -137,7 +134,7 @@ describe('RegistrationFailure', () => {
failureCount: 0,
};
const { container } = render(reduxWrapper(<IntlRegistrationFailure {...props} />));
const { container } = render(reduxWrapper(<RegistrationFailureMessage {...props} />));
const alertHeading = container.querySelectorAll('div.alert-heading');
expect(alertHeading.length).toEqual(1);
@@ -153,7 +150,7 @@ describe('RegistrationFailure', () => {
failureCount: 0,
};
const { container } = render(reduxWrapper(<IntlRegistrationFailure {...props} />));
const { container } = render(reduxWrapper(<RegistrationFailureMessage {...props} />));
const alertHeading = container.querySelectorAll('div.alert-heading');
expect(alertHeading.length).toEqual(1);
@@ -172,7 +169,7 @@ describe('RegistrationFailure', () => {
failureCount: 0,
};
const { container } = render(reduxWrapper(<IntlRegistrationFailure {...props} />));
const { container } = render(reduxWrapper(<RegistrationFailureMessage {...props} />));
const alertHeading = container.querySelectorAll('div.alert-heading');
expect(alertHeading.length).toEqual(1);
@@ -191,7 +188,7 @@ describe('RegistrationFailure', () => {
failureCount: 0,
};
const { container } = render(reduxWrapper(<IntlRegistrationFailure {...props} />));
const { container } = render(reduxWrapper(<RegistrationFailureMessage {...props} />));
const alertHeading = container.querySelectorAll('div.alert-heading');
expect(alertHeading.length).toEqual(1);
@@ -211,7 +208,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.');
expect(validationError).not.toBeNull();

View File

@@ -1,9 +1,8 @@
import React from 'react';
import { Provider } from 'react-redux';
import { getConfig, mergeConfig } from '@edx/frontend-platform';
import {
configure, getLocale, injectIntl, IntlProvider,
configure, getLocale, IntlProvider,
} from '@edx/frontend-platform/i18n';
import { fireEvent, render } from '@testing-library/react';
import { BrowserRouter as Router } from 'react-router-dom';
@@ -23,7 +22,6 @@ jest.mock('@edx/frontend-platform/i18n', () => ({
getLocale: jest.fn(),
}));
const IntlRegistrationPage = injectIntl(RegistrationPage);
const mockStore = configureStore();
jest.mock('react-router-dom', () => {
@@ -157,7 +155,7 @@ describe('ThirdPartyAuth', () => {
});
const { queryByLabelText } = render(
routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />, { store })),
routerWrapper(reduxWrapper(<RegistrationPage {...props} />, { store })),
);
const passwordField = queryByLabelText('Password');
@@ -182,7 +180,7 @@ describe('ThirdPartyAuth', () => {
window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: `?next=/dashboard&tpa_hint=${ssoProvider.id}` };
const { container } = render(
routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)),
routerWrapper(reduxWrapper(<RegistrationPage {...props} />)),
);
const tpaButton = container.querySelector(`button#${ssoProvider.id}`);
@@ -207,7 +205,7 @@ describe('ThirdPartyAuth', () => {
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');
expect(skeletonElement).toBeTruthy();
@@ -231,7 +229,7 @@ describe('ThirdPartyAuth', () => {
window.location = { href: getConfig().BASE_URL.concat(REGISTER_PAGE), search: `?next=/dashboard&tpa_hint=${ssoProvider.id}` };
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`);
expect(iconElement).toBeTruthy();
@@ -254,7 +252,7 @@ describe('ThirdPartyAuth', () => {
delete window.location;
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);
});
@@ -275,7 +273,7 @@ describe('ThirdPartyAuth', () => {
delete window.location;
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`);
expect(providerButton.textContent).toEqual(expectedMessage);
@@ -294,7 +292,7 @@ describe('ThirdPartyAuth', () => {
});
const { container } = render(
routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />, { store })),
routerWrapper(reduxWrapper(<RegistrationPage {...props} />, { store })),
);
const buttonsWithId = container.querySelectorAll(`button#${ssoProvider.id}`);
@@ -315,7 +313,7 @@ describe('ThirdPartyAuth', () => {
});
const { container } = render(
routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)),
routerWrapper(reduxWrapper(<RegistrationPage {...props} />)),
);
const buttonsWithId = container.querySelectorAll(`button#${ssoProvider.id}`);
@@ -329,7 +327,7 @@ describe('ThirdPartyAuth', () => {
institutionLogin: true,
};
const { getByText } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
const { getByText } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
const headingElement = getByText('Register with institution/campus credentials');
expect(headingElement).toBeTruthy();
});
@@ -354,7 +352,7 @@ describe('ThirdPartyAuth', () => {
window.location = { href: getConfig().BASE_URL };
const { container } = render(
routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)),
routerWrapper(reduxWrapper(<RegistrationPage {...props} />)),
);
const ssoButton = container.querySelector('button#oa2-apple-id');
@@ -385,7 +383,7 @@ describe('ThirdPartyAuth', () => {
delete window.location;
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);
});
@@ -406,7 +404,7 @@ describe('ThirdPartyAuth', () => {
const expectedMessage = `${'You\'ve successfully signed into Apple! We just need a little more information before '
+ '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');
expect(tpaAlert.textContent).toEqual(expectedMessage);
});
@@ -437,7 +435,7 @@ describe('ThirdPartyAuth', () => {
store.dispatch = jest.fn(store.dispatch);
const { container } = render(
routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)),
routerWrapper(reduxWrapper(<RegistrationPage {...props} />)),
);
const alertHeading = container.querySelector('div.alert-heading');

View File

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

View File

@@ -1,11 +1,14 @@
import { camelCaseObject } from '@edx/frontend-platform';
import { logError, logInfo } from '@edx/frontend-platform/logging';
import { call, put, takeEvery } from 'redux-saga/effects';
import {
call, put, race, take, takeEvery,
} from 'redux-saga/effects';
import {
fetchRealtimeValidationsBegin,
fetchRealtimeValidationsFailure,
fetchRealtimeValidationsSuccess,
REGISTER_CLEAR_USERNAME_SUGGESTIONS,
REGISTER_FORM_VALIDATIONS,
REGISTER_NEW_USER,
registerNewUserBegin,
@@ -41,9 +44,15 @@ export function* handleNewUserRegistration(action) {
export function* fetchRealtimeValidations(action) {
try {
yield put(fetchRealtimeValidationsBegin());
const { fieldValidations } = yield call(getFieldsValidations, action.payload.formPayload);
yield put(fetchRealtimeValidationsSuccess(camelCaseObject(fieldValidations)));
const { response } = yield race({
response: call(getFieldsValidations, action.payload.formPayload),
cancel: take(REGISTER_CLEAR_USERNAME_SUGGESTIONS),
});
if (response) {
yield put(fetchRealtimeValidationsSuccess(camelCaseObject(response.fieldValidations)));
}
} catch (e) {
if (e.response && e.response.status === 403) {
yield put(fetchRealtimeValidationsFailure());

View File

@@ -0,0 +1,77 @@
import { isFormValid } from '../utils';
describe('Payload validation', () => {
let formatMessage;
let configurableFormFields;
let fieldDescriptions;
beforeEach(() => {
formatMessage = jest.fn(msg => msg);
configurableFormFields = {
confirm_email: true,
};
fieldDescriptions = {};
});
test('validates name field correctly', () => {
const payload = { name: ' ' };
const errors = {};
const { isValid, fieldErrors } = isFormValid(
payload,
errors,
configurableFormFields,
fieldDescriptions,
formatMessage);
expect(fieldErrors.name).toBeDefined();
expect(isValid).toBe(false);
});
test('validates email field correctly', () => {
const payload = { email: 'invalid-email' };
const errors = {};
const { isValid, fieldErrors } = isFormValid(
payload, errors, configurableFormFields, fieldDescriptions, formatMessage);
expect(fieldErrors.email).toBeDefined();
expect(isValid).toBe(false);
});
test('validates username field correctly', () => {
const payload = { username: 'invalid username' };
const errors = {};
const { isValid, fieldErrors } = isFormValid(
payload, errors, configurableFormFields, fieldDescriptions, formatMessage);
expect(fieldErrors.username).toBeDefined();
expect(isValid).toBe(false);
});
test('validates password field correctly', () => {
const payload = { password: 'short' };
const errors = {};
const { isValid, fieldErrors } = isFormValid(
payload, errors, configurableFormFields, fieldDescriptions, formatMessage);
expect(fieldErrors.password).toBeDefined();
expect(isValid).toBe(false);
});
test('validates multiple fields correctly', () => {
const payload = {
name: 'InvalidName!',
email: 'invalid-email',
username: 'invalid username',
password: 'short',
};
const errors = {};
const { isValid, fieldErrors } = isFormValid(
payload, errors, configurableFormFields, fieldDescriptions, formatMessage);
expect(fieldErrors.name).toBeDefined();
expect(fieldErrors.email).toBeDefined();
expect(fieldErrors.username).toBeDefined();
expect(fieldErrors.password).toBeDefined();
expect(isValid).toBe(false);
});
});

View File

@@ -43,30 +43,39 @@ export const isFormValid = (
Object.keys(payload).forEach(key => {
switch (key) {
case 'name':
fieldErrors.name = validateName(payload.name, formatMessage);
if (!fieldErrors.name) {
fieldErrors.name = validateName(payload.name, formatMessage);
}
if (fieldErrors.name) { isValid = false; }
break;
case 'email': {
const {
fieldError, confirmEmailError, suggestion,
} = validateEmail(payload.email, configurableFormFields?.confirm_email, formatMessage);
if (fieldError) {
fieldErrors.email = fieldError;
isValid = false;
if (!fieldErrors.email) {
const {
fieldError, confirmEmailError, suggestion,
} = validateEmail(payload.email, configurableFormFields?.confirm_email, formatMessage);
if (fieldError) {
fieldErrors.email = fieldError;
isValid = false;
}
if (confirmEmailError) {
fieldErrors.confirm_email = confirmEmailError;
isValid = false;
}
emailSuggestion = suggestion;
}
if (confirmEmailError) {
fieldErrors.confirm_email = confirmEmailError;
isValid = false;
}
emailSuggestion = suggestion;
if (fieldErrors.email) { isValid = false; }
break;
}
case 'username':
fieldErrors.username = validateUsername(payload.username, formatMessage);
if (!fieldErrors.username) {
fieldErrors.username = validateUsername(payload.username, formatMessage);
}
if (fieldErrors.username) { isValid = false; }
break;
case 'password':
fieldErrors.password = validatePasswordField(payload.password, formatMessage);
if (!fieldErrors.password) {
fieldErrors.password = validatePasswordField(payload.password, formatMessage);
}
if (fieldErrors.password) { isValid = false; }
break;
default:
@@ -125,8 +134,8 @@ export const prepareRegistrationPayload = (
delete payload.marketingEmailsOptIn;
}
payload = snakeCaseObject(payload);
payload.totalRegistrationTime = totalRegistrationTime;
payload = snakeCaseObject(payload);
// add query params to the payload
payload = { ...payload, ...queryParams };

View File

@@ -162,6 +162,11 @@ const messages = defineMessages({
defaultMessage: 'Registration using {provider} has timed out.',
description: '',
},
'registration.forbidden.username': {
id: 'registration.forbidden.username',
defaultMessage: 'Usernames can\'t include words that could be mistaken for course roles. Please choose a different username.',
description: '',
},
'registration.tpa.authentication.failure': {
id: 'registration.tpa.authentication.failure',
defaultMessage: 'We are sorry, you are not authorized to access {platform_name} via this channel. '

View File

@@ -1,7 +1,6 @@
import React from 'react';
import { Provider } from 'react-redux';
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';
@@ -26,7 +25,6 @@ jest.mock('react-router-dom', () => ({
useParams: jest.fn().mockReturnValue({ token }),
}));
const IntlResetPasswordPage = injectIntl(ResetPasswordPage);
const mockStore = configureStore();
describe('ResetPasswordPage', () => {
@@ -95,7 +93,7 @@ describe('ResetPasswordPage', () => {
}));
store.dispatch = jest.fn(store.dispatch);
render(reduxWrapper(<IntlResetPasswordPage {...props} />));
render(reduxWrapper(<ResetPasswordPage {...props} />));
const newPasswordInput = screen.getByLabelText('New password');
const confirmPasswordInput = screen.getByLabelText('Confirm password');
@@ -120,7 +118,7 @@ describe('ResetPasswordPage', () => {
status: TOKEN_STATE.VALID,
},
});
render(reduxWrapper(<IntlResetPasswordPage {...props} />));
render(reduxWrapper(<ResetPasswordPage {...props} />));
const resetPasswordButton = screen.getByRole('button', { name: /Reset password/i, id: 'submit-new-password' });
fireEvent.click(resetPasswordButton);
@@ -144,7 +142,7 @@ describe('ResetPasswordPage', () => {
status: TOKEN_STATE.VALID,
},
});
render(reduxWrapper(<IntlResetPasswordPage {...props} />));
render(reduxWrapper(<ResetPasswordPage {...props} />));
const confirmPasswordInput = screen.getByLabelText('Confirm password');
fireEvent.change(confirmPasswordInput, { target: { value: 'password-mismatch' } });
@@ -163,7 +161,7 @@ describe('ResetPasswordPage', () => {
},
});
const { container } = render(reduxWrapper(<IntlResetPasswordPage {...props} />));
const { container } = render(reduxWrapper(<ResetPasswordPage {...props} />));
const alertElements = container.querySelectorAll('.alert-danger');
const rateLimitError = alertElements[0].textContent;
@@ -179,7 +177,7 @@ describe('ResetPasswordPage', () => {
},
});
const { container } = render(reduxWrapper(<IntlResetPasswordPage {...props} />));
const { container } = render(reduxWrapper(<ResetPasswordPage {...props} />));
const alertElements = container.querySelectorAll('.alert-danger');
const internalServerError = alertElements[0].textContent;
expect(internalServerError).toBe(validationMessage);
@@ -188,7 +186,7 @@ describe('ResetPasswordPage', () => {
// ******** miscellaneous tests ********
it('should call validation on password field when blur event fires', () => {
const resetPasswordPage = render(reduxWrapper(<IntlResetPasswordPage {...props} />));
const resetPasswordPage = render(reduxWrapper(<ResetPasswordPage {...props} />));
const expectedText = 'Password criteria has not been metPassword must contain at least 8 characters, at least one letter, and at least one number';
const newPasswordInput = resetPasswordPage.container.querySelector('input#newPassword');
newPasswordInput.value = 'test-password';
@@ -207,7 +205,7 @@ describe('ResetPasswordPage', () => {
TOKEN_STATE.PENDING,
};
render(reduxWrapper(<IntlResetPasswordPage {...props} />));
render(reduxWrapper(<ResetPasswordPage {...props} />));
expect(store.dispatch).toHaveBeenCalledWith(validateToken(token));
});
@@ -216,19 +214,19 @@ describe('ResetPasswordPage', () => {
status:
PASSWORD_RESET_ERROR,
};
render(reduxWrapper(<IntlResetPasswordPage {...props} />));
render(reduxWrapper(<ResetPasswordPage {...props} />));
expect(mockedNavigator).toHaveBeenCalledWith(RESET_PAGE);
});
it('should redirect the user to root url of the application ', async () => {
props = {
status: SUCCESS,
};
render(reduxWrapper(<IntlResetPasswordPage {...props} />));
render(reduxWrapper(<ResetPasswordPage {...props} />));
expect(mockedNavigator).toHaveBeenCalledWith(LOGIN_PAGE);
});
it('shows spinner during token validation', () => {
render(reduxWrapper(<IntlResetPasswordPage {...props} />));
render(reduxWrapper(<ResetPasswordPage {...props} />));
const spinnerElement = document.getElementsByClassName('div.spinner-header');
expect(spinnerElement).toBeTruthy();
@@ -237,7 +235,7 @@ describe('ResetPasswordPage', () => {
// ******** redirection tests ********
it('by clicking on sign in tab should redirect onto login page', async () => {
const { getByText } = render(reduxWrapper(<IntlResetPasswordPage {...props} />));
const { getByText } = render(reduxWrapper(<ResetPasswordPage {...props} />));
const signInTab = getByText('Sign in');

View File

@@ -2,19 +2,19 @@
.layout {
display: flex;
@include media-breakpoint-down('lg') {
@media (--pgn-size-breakpoint-max-width-lg) {
flex-direction: column;
justify-content: center;
align-items: center;
}
@include media-breakpoint-up('xl') {
@media (--pgn-size-breakpoint-min-width-xl) {
justify-content: space-between;
}
}
.content {
@include media-breakpoint-up('xl') {
@media (--pgn-size-breakpoint-min-width-xl) {
display: flex;
justify-content: center;
width: 50vw;
@@ -47,7 +47,7 @@
font-weight: 700;
line-height: 1;
@include media-breakpoint-down('xl') {
@media (--pgn-size-breakpoint-max-width-xl) {
font-size: 3.75rem;
}
@@ -60,7 +60,7 @@
margin-bottom: 0.5rem;
font-weight: 700;
@include media-breakpoint-down('xl') {
@media (-pgn-size-breakpoint-max-width-xl) {
font-size: 1.375rem;
line-height: 1.75rem;
}
@@ -72,7 +72,7 @@
}
.large-screen-left-container {
@include media-breakpoint-down('xl') {
@media (-pgn-size-breakpoint-max-width-xl) {
flex: 0 0 25%;
max-width: 25%;
}
@@ -87,43 +87,43 @@
height: 0.25rem;
background-image: linear-gradient(
102.02deg,
$brand-700,
$brand-700 20%,
$brand 20%,
var(--pgn-color-brand-700),
var(--pgn-color-brand-700) 20%,
var(--pgn-color-brand-base) 20%,
);
background-repeat: no-repeat;
}
@include media-breakpoint-only('md') {
@media (--pgn-size-breakpoint-min-width-md) and (--pgn-size-breakpoint-max-width-md) {
.medium-screen-top-stripe {
display: flex;
height: 0.5rem;
background-image: linear-gradient(
102.02deg,
$brand-700,
$brand-700 10%,
$brand 10%,
$brand 90%,
$primary-700 90%,
$primary-700 100%,
var(--pgn-color-brand-700),
var(--pgn-color-brand-700) 10%,
var(--pgn-color-brand-base) 10%,
var(--pgn-color-brand-base) 90%,
var(--pgn-color-primary-700) 90%,
var(--pgn-color-primary-700) 100%,
);
background-repeat: no-repeat;
}
}
@include media-breakpoint-only('lg') {
@media (--pgn-size-breakpoint-min-width-lg) and (--pgn-size-breakpoint-max-width-lg){
.medium-screen-top-stripe {
display: flex;
height: 0.5rem;
background-image: linear-gradient(
102.02deg,
$brand-700 10%,
$brand 10%,
$brand 65%,
$primary-700 65%,
$primary-700 75%,
$accent-a 75%,
$accent-a 75%
var(--pgn-color-brand-700) 10%,
var(--pgn-color-brand-base) 10%,
var(--pgn-color-brand-base) 65%,
var(--pgn-color-primary-700) 65%,
var(--pgn-color-primary-700) 75%,
var(--pgn-color-accent-a) 75%,
var(--pgn-color-accent-a) 75%
);
background-repeat: no-repeat;
}
@@ -131,20 +131,20 @@
.extra-large-screen-top-stripe { display: none; }
@include media-breakpoint-up('xl') {
@media (--pgn-size-breakpoint-min-width-xl) {
.extra-large-screen-top-stripe {
display: flex;
height: 0.5rem;
background-image: linear-gradient(
102.02deg,
$brand-700 10%,
$brand 10%,
$brand 45%,
$primary-700 45%,
$primary-700 55%,
$accent-a 55%,
$accent-a 75%,
$info-200 75%,
var(--pgn-color-brand-700) 10%,
var(--pgn-color-brand-base) 10%,
var(--pgn-color-brand-base) 45%,
var(--pgn-color-primary-700) 45%,
var(--pgn-color-primary-700) 55%,
var(--pgn-color-accent-a) 55%,
var(--pgn-color-accent-a) 75%,
var(--pgn-color-info-200) 75%,
);
background-repeat: no-repeat;
}
@@ -152,24 +152,24 @@
.large-screen-svg-light,
.large-screen-svg-primary {
fill: $light-200;
fill: var(--pgn-color-light-200);
overflow: hidden;
position: absolute;
}
.large-screen-svg-primary {
fill: $primary-400;
fill: var(--pgn-color-primary-400);
}
.medium-screen-svg-light,
.medium-screen-svg-primary {
fill: $light-200;
fill: var(--pgn-color-light-200);
overflow: inherit;
position: absolute;
}
.medium-screen-svg-primary {
fill: $primary-400;
fill: var(--pgn-color-primary-400);
}
[dir=rtl]{
@@ -184,20 +184,20 @@
.small-yellow-line {
width: 80px;
height: 0;
border: 2px solid $accent-b;
border: 2px solid var(--pgn-color-accent-b);
transform: rotate(102.02deg);
}
.medium-yellow-line {
width: 120px;
height: 0;
border: 3px solid $accent-b;
border: 3px solid var(--pgn-color-accent-b);
transform: rotate(102.02deg);
}
.large-yellow-line {
width: 240px;
height: 0;
border: 3px solid $accent-b;
border: 3px solid var(--pgn-color-accent-b);
transform: rotate(102.02deg);
}

View File

@@ -11,7 +11,7 @@
margin-bottom: 0.5rem;
font-weight: 700;
@include media-breakpoint-down('md') {
@media (--pgn-size-breakpoint-max-width-md) {
line-height: 1.5rem;
font-size: 1.125rem;
}

View File

@@ -64,52 +64,52 @@ $header-height: 104px;
}
&.light {
background-color: $white;
background-color: var(--pgn-color-white);
.title {
color: $black;
color: var(--pgn-color-black);
}
.subtitle {
color: $gray-700;
color: var(--pgn-color-gray-700);
}
.badge {
background-color: $light-500;
color: $black;
background-color: var(--pgn-color-light-500);
color: var(--pgn-color-black);
}
.footer-content {
color: $gray-700;
color: var(--pgn-color-gray-700);
}
}
&.dark {
background-color: $primary-500;
background-color: var(--pgn-color-primary-500);
.pgn__card-header-title-md {
color: $white;
color: var(--pgn-color-white);
}
.pgn__card-header-subtitle-md {
color: $light-200;
color: var(--pgn-color-light-200);
}
.title {
color: $white;
color: var(--pgn-color-white);
}
.subtitle {
color: $light-200;
color: var(--pgn-color-light-200);
}
.badge {
background-color: $dark-200;
color: $white;
background-color: var(--pgn-color-dark-200);
color: var(--pgn-color-white);
}
.footer-content {
color: $light-200;
color: var(--pgn-color-light-200);
}
}
}

View File

@@ -3,7 +3,7 @@ $card-gap: 24px;
.recommendations-container__card-list {
gap: $card-gap $card-gap;
@include media-breakpoint-down(sm) {
@media (-pgn-size-breakpoint-max-width-sm) {
margin-bottom: 0 !important;
}
@@ -11,15 +11,15 @@ $card-gap: 24px;
flex: 0 1 100%;
cursor: pointer;
@include media-breakpoint-up(sm) {
@media (--pgn-size-breakpoint-min-width-sm) {
flex: 0 1 calc(50% - #{$card-gap - 12});
}
@include media-breakpoint-up(md) {
@media (--pgn-size-breakpoint-min-width-md) {
flex: 0 1 calc(33.333% - #{$card-gap - 8});
}
@include media-breakpoint-up(lg) {
@media (--pgn-size-breakpoint-min-width-lg) {
flex: 0 1 calc(25% - #{$card-gap - 6});
}
}

View File

@@ -23,21 +23,21 @@
}
.alert-link {
color: $primary !important;
color: var(--pgn-color-primary-base) !important;
&:hover {
text-decoration: underline;
color: $info-700 !important;
color: var(--pgn-color-info-700) !important;
}
}
}
.email-suggestion-alert-warning {
color: $info-500 !important;
color: var(--pgn-color-info-500) !important;
&:hover {
text-decoration: underline;
color: $info-700 !important;
color: var(--pgn-color-info-700) !important;
}
}
@@ -56,7 +56,7 @@
line-height: 24px;
font-size: 12px;
font-weight: normal;
color: $primary-700;
color: var(--pgn-color-primary-700);
}
.username-suggestion--label {
@@ -65,10 +65,15 @@
margin-right: 0.25rem;
}
.username-suggestions {
.username__form-group-wrapper {
position: relative;
margin-top: -2.5rem;
margin-left: 15px;
}
.username-suggestions {
position: absolute;
inset: 0;
padding-left: 15px;
z-index: 100;
}
.username-suggestions__close__button {
@@ -76,13 +81,6 @@
position: absolute;
}
.username-suggestions__error {
position: relative;
margin-top: -13.7%;
margin-bottom: 11%;
margin-left: 15px;
}
.username-scroll-suggested--form-field {
width: 20rem;
white-space: nowrap;
@@ -99,7 +97,7 @@
}
}
@media (max-width: map-get($grid-breakpoints, "sm")) {
@media (--pgn-size-breakpoint-max-width-sm) {
.username-scroll-suggested--form-field {
width: 15rem;
}

View File

@@ -40,7 +40,7 @@ $elevation-level-2-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.15);
}
.main-content {
@extend .pt-4;
padding-top: calc(var(--pgn-spacing-spacer-base) * 1.5) !important;
min-width: 464px !important;
}
@@ -80,15 +80,15 @@ $elevation-level-2-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.15);
.alert-link {
font-weight: normal;
text-decoration: underline;
color: $info-300 !important;
color: var(--pgn-color-info-300) !important;
&:hover {
color: $info-500 !important;
color: var(--pgn-color-info-500) !important;
}
}
.form-control {
background-color: $white !important;
background-color: var(--pgn-color-white) !important;
font-size: 0.875rem;
line-height: 1.5;
height: 2.75rem;
@@ -103,11 +103,11 @@ $elevation-level-2-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.15);
margin-bottom: 1rem;
font-size: 14px;
background-color: $white;
border: 1px solid $primary;
background-color: var(--pgn-color-white);
border: 1px solid var(--pgn-color-primary-base);
width: 224px;
height: 36px;
color: $primary;
color: var(--pgn-color-primary-base);
.btn-tpa__image-icon{
background-color: transparent;
@@ -132,8 +132,8 @@ $elevation-level-2-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.15);
}
.btn-tpa__font-container {
background-color: $primary;
color: $white;
background-color: var(--pgn-color-primary-base);
color: var(--pgn-color-white);
font-size: 11px;
margin-left: -6px;
@@ -143,7 +143,7 @@ $elevation-level-2-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.15);
}
.btn-oa2-facebook {
color: $white;
color: var(--pgn-color-white);
border-color: $facebook-blue;
background-color: $facebook-blue;
@@ -151,12 +151,12 @@ $elevation-level-2-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.15);
&:focus {
background-color: $facebook-focus-blue;
border: 1px solid $facebook-focus-blue;
color: $white;
color: var(--pgn-color-white);
}
}
.btn-oa2-google-oauth2 {
color: $white;
color: var(--pgn-color-white);
border-color: $google-blue;
background-color: $google-blue;
@@ -171,12 +171,12 @@ $elevation-level-2-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.15);
&:focus {
background-color: $google-focus-blue;
border: 1px solid $google-focus-blue;
color: $white;
color: var(--pgn-color-white);
}
}
.btn-oa2-apple-id {
color: $white;
color: var(--pgn-color-white);
border-color: $apple-black;
background-color: $apple-black;
font-size: 16px;
@@ -190,12 +190,12 @@ $elevation-level-2-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.15);
&:focus {
background-color: $apple-focus-black;
border: 1px solid $apple-focus-black;
color: $white;
color: var(--pgn-color-white);
}
}
.btn-oa2-azuread-oauth2 {
color: $white;
color: var(--pgn-color-white);
border-color: $microsoft-black;
background-color: $microsoft-black;
@@ -203,7 +203,7 @@ $elevation-level-2-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.15);
&:focus {
background-color: $microsoft-focus-black;
border: 1px solid $microsoft-focus-black;
color: $white;
color: var(--pgn-color-white);
}
}
@@ -214,9 +214,8 @@ $elevation-level-2-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.15);
}
.institute-icon {
@extend .mr-1;
@extend .text-gray;
margin: calc(var(--pgn-spacing-spacer-base) * 0.25) !important;
color: var(--pgn-color-gray-base) !important;
display: inline-block;
margin-bottom: 0.25rem;
height: 18px;
@@ -232,7 +231,7 @@ $elevation-level-2-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.15);
}
.invalid-feedback {
color: $red;
color: var(--pgn-color-red);
}
.full-vertical-height {
@@ -290,22 +289,22 @@ select.form-control {
#password-requirement-left {
opacity: 1;
@extend .x-small;
font-size: var(--pgn-typography-font-size-xs) !important;
filter: drop-shadow($elevation-level-2-shadow) drop-shadow($elevation-level-2-shadow) !important;
right: 0.2rem !important;
.tooltip-inner {
background: $white;
background: var(--pgn-color-white);
display: block;
color: $gray-500;
color: var(--pgn-color-gray-500);
}
.arrow::before {
border-left-color: $white;
border-left-color: var(--pgn-color-white);
}
}
#password-requirement-top {
@extend .x-small;
filter: drop-shadow($elevation-level-2-shadow) drop-shadow($elevation-level-2-shadow) !important;
font-size: var(--pgn-typography-font-size-xs) !important;
filter: drop-shadow(var(--pgn-elevation-box-shadow-level-2)) drop-shadow(var(--pgn-elevation-box-shadow-level-2)) !important;
opacity: 1;
width: 90%;
bottom: 10px !important;
@@ -314,30 +313,30 @@ select.form-control {
.tooltip-inner {
min-width: 464px !important;
background: $white;
background: var(--pgn-color-white);
display: block;
color: $gray-500;
color: var(--pgn-color-gray-500);
}
.arrow::before {
border-top-color: $white;
border-top-color: var(--pgn-color-white);
}
}
.yellow-border {
border: 2px solid $accent-b;
border: 2px solid var(--pgn-color-accent-b);
}
.institutions__heading {
color: $primary-700;
color: var(--pgn-color-primary-700);
}
.logistration-button {
color: $gray-700;
color: var(--pgn-color-gray-700);
}
.logistration-button:hover{
color: $gray-700;
color: var(--pgn-color-gray-700);
text-decoration: none;
}
@@ -352,7 +351,7 @@ select.form-control {
width: 2.3rem;
}
.has-floating-label {
color: $gray-500;
color: var(--pgn-color-gray-500);
}
.pgn__form-control-floating-label .pgn__form-control-floating-label-content {
@@ -366,7 +365,7 @@ select.form-control {
.form-group__form-field .form-control:focus ~ .pgn__form-control-floating-label .pgn__form-control-floating-label-content {
font-size: 16px;
color: $primary-700;
color: var(--pgn-color-primary-700);
}
.form-group__form-field .form-control:not([value='']):not(:focus) ~
@@ -444,14 +443,14 @@ select.form-control {
}
.table-striped tbody tr:nth-of-type(odd) {
background-color: $light-200;
background-color: var(--pgn-color-light-200);
}
.institutions--provider-link {
font-weight: normal;
font-size: 0.875rem;
line-height: 1.5rem;
color: $primary-700
color: var(--pgn-color-primary-700)
}
.pgn__form-control-decorator-trailing {