Compare commits

..

50 Commits

Author SHA1 Message Date
Muhammad Abdullah Waheed
826f631201 Merge pull request #1319 from openedx/abdullahwaheed/update-2u-main-temp
[test] Abdullahwaheed/update 2u main temp
2024-09-13 18:03:08 +05:00
Awais Ansari
b41fca3605 feat: removed Russian Federation from country list (#1315) 2024-09-12 10:01:48 +05:00
Mubbshar Anwar
ac2548913f fix: password reset redirection (#1300)
fix authenticated user redirects to 404 if token is invalide for password reset
VAN-2052
2024-09-12 10:01:48 +05:00
Blue
cd9b3bd084 fix: covert totalRegistrationTime to snake case (#1302)
Description:
Convert totalRegistrationTime to snake case
VAN-1816

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

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

* feat: added utils for tracking events

* refactor: mapped login events

* refactor: mapped forgot password events

* refactor: mapped reset password events

* refactor: mapped register events

* fix: fixed unit tests

* refactor: mapped progressive prifiling events

* fix: fixed unit tests

* refactor: added app name in logistration events

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

VAN-1971

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

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

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

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

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

* feat: implement multi step registration experiment

Rebase 2u main with master (#1228)

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

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

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

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

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

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

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

---------

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

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

* feat: implement multi step registration experiment

* feat: add multi step registration eventing

* fix: fix register button width

* fix: fix register button loader for control

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

* revert: multistep registration experiment
revert multistep registration experiment changes

VAN-1930

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

* feat: implement auto generated username registration exp

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

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

---------

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

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

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

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

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

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

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

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

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

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-11 18:23:56 +05:00
Awais Ansari
c1e63da778 feat: removed Russian Federation from country list (#1315) 2024-09-10 21:11:44 +05:00
Mubbshar Anwar
ecf4c3ae53 fix: password reset redirection (#1300)
fix authenticated user redirects to 404 if token is invalide for password reset
VAN-2052
2024-08-29 09:48:21 +05:00
Blue
2428b4c389 fix: covert totalRegistrationTime to snake case (#1302)
Description:
Convert totalRegistrationTime to snake case
VAN-1816

Co-authored-by: Ahtesham Quraish <ahtesham.quraish@192.168.1.4>
2024-08-28 14:58:40 +05:00
Syed Sajjad Hussain Shah
099fe8d717 fix: fix datadog js errors (#1296) 2024-08-01 16:06:20 +05:00
Syed Sajjad Hussain Shah
4755540be8 fix: retain query params in authenticated user redirection (#1288) 2024-07-29 11:23:48 +05:00
Mubbshar Anwar
9a30f053c7 fix: fix marketingEmailsOptIn null value (#1294)
Fix marketingEmailsOptIn null value issue for SSO flow on onboarding component

VAN-2013
2024-07-26 15:57:18 +05:00
Mubbshar Anwar
6b983e18d3 fix: remove cookie (#1286)
-remove marketingEmailsOptIn cookie on successful registration
- fix tests
2024-07-12 17:05:50 +05:00
Syed Sajjad Hussain Shah
327210192c fix: set marketing opt in in cookie for sso (#1285) 2024-07-12 13:18:42 +05:00
Muhammad Abdullah Waheed
0d603b5fa1 feat: added app name identifier in segment events (#1277)
* feat: added app name identifier in registration call

* feat: added utils for tracking events

* refactor: mapped login events

* refactor: mapped forgot password events

* refactor: mapped reset password events

* refactor: mapped register events

* fix: fixed unit tests

* refactor: mapped progressive prifiling events

* fix: fixed unit tests

* refactor: added app name in logistration events

* refactor: resolved PR reviews and fixed tests
2024-07-03 17:08:44 +05:00
Mubbshar Anwar
efaa83a1bc feat: hard code fields on frontend (#1256)
* feat: hard code fields
hard code configurable fields on frontend which includes country field on register page & level of education & gender field on progressive profiling

VAN-1971

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

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

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

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

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

* feat: implement multi step registration experiment

Rebase 2u main with master (#1228)

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

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

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

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

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

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

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

---------

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

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

* feat: implement multi step registration experiment

* feat: add multi step registration eventing

* fix: fix register button width

* fix: fix register button loader for control

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

* revert: multistep registration experiment
revert multistep registration experiment changes

VAN-1930

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

* feat: implement auto generated username registration exp

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

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

---------

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

VAN-1930
2024-05-07 11:47:28 +05:00
Syed Sajjad Hussain Shah
03d1666c2c feat: capture marketing lead in experiment events (#1243) 2024-04-25 15:28:05 +05:00
Syed Sajjad Hussain Shah
3782503983 fix: fix register button loader for control 2024-04-22 16:53:23 +05:00
Syed Sajjad Hussain Shah
b219fe3683 fix: fix register button width 2024-04-22 14:37:36 +05:00
Syed Sajjad Hussain Shah
90f650ce3e feat: add multi step registration eventing (#1226)
* feat: implement multi step registration experiment

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

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

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

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

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

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

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

---------

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

2
.env
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,15 +10,18 @@ on:
jobs: jobs:
tests: tests:
runs-on: ubuntu-latest runs-on: ubuntu-20.04
strategy:
matrix:
node: [18, 20]
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v3
- name: Setup Nodejs - name: Setup Nodejs
uses: actions/setup-node@v6 uses: actions/setup-node@v4
with: with:
node-version-file: '.nvmrc' node-version: ${{ matrix.node }}
- name: Install Dependencies - name: Install Dependencies
run: npm ci run: npm ci
@@ -39,7 +42,7 @@ jobs:
run: npm run build run: npm run build
- name: Run Code Coverage - name: Run Code Coverage
uses: codecov/codecov-action@v5 uses: codecov/codecov-action@v4
with: with:
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true fail_ci_if_error: true

2
.nvmrc
View File

@@ -1 +1 @@
24 20

2
CODEOWNERS Normal file
View File

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

View File

@@ -29,13 +29,7 @@ Getting Started
Installation Installation
============ ============
`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. 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: https://github.com/overhangio/tutor
.. _relevant tutor-mfe documentation: https://github.com/overhangio/tutor-mfe?tab=readme-ov-file#mfe-development
Devstack (Deprecated) instructions
==================================
1. Install Devstack using the `Getting Started <https://github.com/openedx/devstack#getting-started>`_ instructions. 1. Install Devstack using the `Getting Started <https://github.com/openedx/devstack#getting-started>`_ instructions.
@@ -57,7 +51,7 @@ Devstack (Deprecated) instructions
Environment Variables/Setup Notes Environment Variables/Setup Notes
================================= =================================
This MFE is configured via environment variables supplied at build time. All micro-frontends have a shared set of required environment variables, as documented in the Open edX Developer Guide under `Required Environment Variables <https://github.com/overhangio/tutor-mfe?tab=readme-ov-file#mfe-development>`__. 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>`__.
The authentication micro-frontend also requires the following additional variable: The authentication micro-frontend also requires the following additional variable:
@@ -148,13 +142,13 @@ Furthermore, there are several edX-specific environment variables that enable in
- ``true`` | ``''`` (empty strings are falsy) - ``true`` | ``''`` (empty strings are falsy)
For more information see the document: `Micro-frontend applications in Open For more information see the document: `Micro-frontend applications in Open
edX <https://github.com/overhangio/tutor-mfe?tab=readme-ov-file#mfe-development>`__. edX <https://edx.readthedocs.io/projects/edx-developer-docs/en/latest/developers_guide/micro_frontends_in_open_edx.html#required-environment-variables>`__.
How To Contribute How To Contribute
================= =================
Contributions are very welcome, and strongly encouraged! We've Contributions are very welcome, and strongly encouraged! We've
put together `some documentation that describes our contribution process <https://docs.openedx.org/en/latest/developers/references/developer_guide/process/index.html>`_. put together `some documentation that describes our contribution process <https://edx.readthedocs.org/projects/edx-developer-guide/en/latest/process/index.html>`_.
Even though they were written with edx-platform in mind, the guidelines should be followed for Open edX code in general. Even though they were written with edx-platform in mind, the guidelines should be followed for Open edX code in general.

View File

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

View File

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

8
openedx.yaml Normal file
View File

@@ -0,0 +1,8 @@
# 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-infinity
openedx-release:
ref: master

24219
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -5,7 +5,7 @@ import { Navigate } from 'react-router-dom';
import { import {
AUTHN_PROGRESSIVE_PROFILING, RECOMMENDATIONS, REDIRECT, AUTHN_PROGRESSIVE_PROFILING, RECOMMENDATIONS, REDIRECT,
} from '../data/constants'; } from '../data/constants';
import { setCookie } from '../data/utils'; import setCookie from '../data/utils/cookies';
const RedirectLogistration = (props) => { const RedirectLogistration = (props) => {
const { const {

View File

@@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import { useSelector } from 'react-redux';
import { getConfig } from '@edx/frontend-platform'; import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n'; import { useIntl } from '@edx/frontend-platform/i18n';
@@ -8,15 +9,20 @@ import { Login } from '@openedx/paragon/icons';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import messages from './messages'; import messages from './messages';
import { LOGIN_PAGE, SUPPORTED_ICON_CLASSES } from '../data/constants'; import { LOGIN_PAGE, REGISTER_PAGE, SUPPORTED_ICON_CLASSES } from '../data/constants';
import { setCookie } from '../data/utils';
const SocialAuthProviders = (props) => { const SocialAuthProviders = (props) => {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const { referrer, socialAuthProviders } = props; const { referrer, socialAuthProviders } = props;
const registrationFields = useSelector(state => state.register.registrationFormData);
function handleSubmit(e) { function handleSubmit(e) {
e.preventDefault(); e.preventDefault();
if (referrer === REGISTER_PAGE) {
setCookie('marketingEmailsOptIn', registrationFields?.configurableFormFields?.marketingEmailsOptIn);
}
const url = e.currentTarget.dataset.providerUrl; const url = e.currentTarget.dataset.providerUrl;
window.location.href = getConfig().LMS_BASE_URL + url; window.location.href = getConfig().LMS_BASE_URL + url;
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,6 +11,7 @@ const configuration = {
MARKETING_EMAILS_OPT_IN: process.env.MARKETING_EMAILS_OPT_IN || '', MARKETING_EMAILS_OPT_IN: process.env.MARKETING_EMAILS_OPT_IN || '',
SHOW_CONFIGURABLE_EDX_FIELDS: process.env.SHOW_CONFIGURABLE_EDX_FIELDS || false, SHOW_CONFIGURABLE_EDX_FIELDS: process.env.SHOW_CONFIGURABLE_EDX_FIELDS || false,
SHOW_REGISTRATION_LINKS: process.env.SHOW_REGISTRATION_LINKS !== 'false', SHOW_REGISTRATION_LINKS: process.env.SHOW_REGISTRATION_LINKS !== 'false',
ENABLE_HARD_CODE_OPTIONAL_FIELDS: process.env.ENABLE_HARD_CODE_OPTIONAL_FIELDS || false,
ENABLE_IMAGE_LAYOUT: process.env.ENABLE_IMAGE_LAYOUT || false, ENABLE_IMAGE_LAYOUT: process.env.ENABLE_IMAGE_LAYOUT || false,
// Links // Links
ACTIVATION_EMAIL_SUPPORT_LINK: process.env.ACTIVATION_EMAIL_SUPPORT_LINK || null, ACTIVATION_EMAIL_SUPPORT_LINK: process.env.ACTIVATION_EMAIL_SUPPORT_LINK || null,
@@ -35,6 +36,7 @@ const configuration = {
ZENDESK_LOGO_URL: process.env.ZENDESK_LOGO_URL, ZENDESK_LOGO_URL: process.env.ZENDESK_LOGO_URL,
ALGOLIA_APP_ID: process.env.ALGOLIA_APP_ID || '', ALGOLIA_APP_ID: process.env.ALGOLIA_APP_ID || '',
ALGOLIA_SEARCH_API_KEY: process.env.ALGOLIA_SEARCH_API_KEY || '', ALGOLIA_SEARCH_API_KEY: process.env.ALGOLIA_SEARCH_API_KEY || '',
AUTO_GENERATED_USERNAME_EXPERIMENT_ID: process.env.AUTO_GENERATED_USERNAME_EXPERIMENT_ID || '',
}; };
export default configuration; export default configuration;

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,8 @@
import { useEffect, useMemo, useState } from 'react'; import React, { useEffect, useMemo, useState } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { getConfig } from '@edx/frontend-platform'; import { getConfig } from '@edx/frontend-platform';
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics'; import { injectIntl, useIntl } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import { import {
Form, StatefulButton, Form, StatefulButton,
} from '@openedx/paragon'; } from '@openedx/paragon';
@@ -42,7 +41,11 @@ import {
getTpaProvider, getTpaProvider,
updatePathWithQueryParams, updatePathWithQueryParams,
} from '../data/utils'; } from '../data/utils';
import { removeCookie } from '../data/utils/cookies';
import ResetPasswordSuccess from '../reset-password/ResetPasswordSuccess'; import ResetPasswordSuccess from '../reset-password/ResetPasswordSuccess';
import {
trackForgotPasswordLinkClick, trackLoginPageViewed, trackLoginSuccess,
} from '../tracking/trackers/login';
const LoginPage = (props) => { const LoginPage = (props) => {
const { const {
@@ -78,9 +81,18 @@ const LoginPage = (props) => {
const tpaHint = getTpaHint(); const tpaHint = getTpaHint();
useEffect(() => { useEffect(() => {
sendPageEvent('login_and_registration', 'login'); trackLoginPageViewed();
}, []); }, []);
useEffect(() => {
if (loginResult.success) {
trackLoginSuccess();
// Remove this cookie that was set to capture marketingEmailsOptIn for the onboarding component
removeCookie('ssoPipelineRedirectionDone');
}
}, [loginResult]);
useEffect(() => { useEffect(() => {
const payload = { ...queryParams }; const payload = { ...queryParams };
if (tpaHint) { if (tpaHint) {
@@ -170,9 +182,6 @@ const LoginPage = (props) => {
const { name } = event.target; const { name } = event.target;
setErrors(prevErrors => ({ ...prevErrors, [name]: '' })); setErrors(prevErrors => ({ ...prevErrors, [name]: '' }));
}; };
const trackForgotPasswordLinkClick = () => {
sendTrackEvent('edx.bi.password-reset_form.toggled', { category: 'user-engagement' });
};
const { provider, skipHintedLogin } = getTpaProvider(tpaHint, providers, secondaryProviders); const { provider, skipHintedLogin } = getTpaProvider(tpaHint, providers, secondaryProviders);
@@ -365,4 +374,4 @@ export default connect(
loginRequest, loginRequest,
getTPADataFromBackend: getThirdPartyAuthContext, getTPADataFromBackend: getThirdPartyAuthContext,
}, },
)(LoginPage); )(injectIntl(LoginPage));

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,9 @@
import React from 'react';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { getConfig, mergeConfig } from '@edx/frontend-platform'; import { getConfig, mergeConfig } from '@edx/frontend-platform';
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics'; import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
import { IntlProvider } from '@edx/frontend-platform/i18n'; import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import { import {
fireEvent, render, screen, waitFor, fireEvent, render, screen, waitFor,
} from '@testing-library/react'; } from '@testing-library/react';
@@ -10,7 +11,9 @@ import { act } from 'react-dom/test-utils';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
import configureStore from 'redux-mock-store'; import configureStore from 'redux-mock-store';
import { COMPLETE_STATE, LOGIN_PAGE, PENDING_STATE } from '../../data/constants'; import {
APP_NAME, COMPLETE_STATE, LOGIN_PAGE, PENDING_STATE,
} from '../../data/constants';
import { backupLoginFormBegin, dismissPasswordResetBanner, loginRequest } from '../data/actions'; import { backupLoginFormBegin, dismissPasswordResetBanner, loginRequest } from '../data/actions';
import { INTERNAL_SERVER_ERROR } from '../data/constants'; import { INTERNAL_SERVER_ERROR } from '../data/constants';
import LoginPage from '../LoginPage'; import LoginPage from '../LoginPage';
@@ -23,6 +26,7 @@ jest.mock('@edx/frontend-platform/auth', () => ({
getAuthService: jest.fn(), getAuthService: jest.fn(),
})); }));
const IntlLoginPage = injectIntl(LoginPage);
const mockStore = configureStore(); const mockStore = configureStore();
describe('LoginPage', () => { describe('LoginPage', () => {
@@ -86,7 +90,7 @@ describe('LoginPage', () => {
it('should submit form for valid input', () => { it('should submit form for valid input', () => {
store.dispatch = jest.fn(store.dispatch); store.dispatch = jest.fn(store.dispatch);
render(reduxWrapper(<LoginPage {...props} />)); render(reduxWrapper(<IntlLoginPage {...props} />));
fireEvent.change(screen.getByText( fireEvent.change(screen.getByText(
'', '',
@@ -107,7 +111,7 @@ describe('LoginPage', () => {
it('should not dispatch loginRequest on empty form submission', () => { it('should not dispatch loginRequest on empty form submission', () => {
store.dispatch = jest.fn(store.dispatch); store.dispatch = jest.fn(store.dispatch);
render(reduxWrapper(<LoginPage {...props} />)); render(reduxWrapper(<IntlLoginPage {...props} />));
fireEvent.click(screen.getByText( fireEvent.click(screen.getByText(
'', '',
@@ -126,7 +130,7 @@ describe('LoginPage', () => {
}); });
store.dispatch = jest.fn(store.dispatch); store.dispatch = jest.fn(store.dispatch);
render(reduxWrapper(<LoginPage {...props} />)); render(reduxWrapper(<IntlLoginPage {...props} />));
fireEvent.click(screen.getByText( fireEvent.click(screen.getByText(
'', '',
{ selector: '.btn-brand' }, { selector: '.btn-brand' },
@@ -140,7 +144,7 @@ describe('LoginPage', () => {
it('should match state for invalid email (less than 2 characters), on form submission', () => { it('should match state for invalid email (less than 2 characters), on form submission', () => {
store.dispatch = jest.fn(store.dispatch); store.dispatch = jest.fn(store.dispatch);
render(reduxWrapper(<LoginPage {...props} />)); render(reduxWrapper(<IntlLoginPage {...props} />));
fireEvent.change(screen.getByText( fireEvent.change(screen.getByText(
'', '',
@@ -160,7 +164,7 @@ describe('LoginPage', () => {
}); });
it('should show error messages for required fields on empty form submission', () => { it('should show error messages for required fields on empty form submission', () => {
const { container } = render(reduxWrapper(<LoginPage {...props} />)); const { container } = render(reduxWrapper(<IntlLoginPage {...props} />));
fireEvent.click(screen.getByText( fireEvent.click(screen.getByText(
'', '',
{ selector: '.btn-brand' }, { selector: '.btn-brand' },
@@ -174,7 +178,7 @@ describe('LoginPage', () => {
}); });
it('should run frontend validations for emailOrUsername field on form submission', () => { it('should run frontend validations for emailOrUsername field on form submission', () => {
const { container } = render(reduxWrapper(<LoginPage {...props} />)); const { container } = render(reduxWrapper(<IntlLoginPage {...props} />));
fireEvent.change(screen.getByText( fireEvent.change(screen.getByText(
'', '',
@@ -193,7 +197,7 @@ describe('LoginPage', () => {
it('should reset field related error messages on onFocus event', async () => { it('should reset field related error messages on onFocus event', async () => {
store.dispatch = jest.fn(store.dispatch); store.dispatch = jest.fn(store.dispatch);
render(reduxWrapper(<LoginPage {...props} />)); render(reduxWrapper(<IntlLoginPage {...props} />));
await act(async () => { await act(async () => {
// clicking submit button with empty fields to make the errors appear // clicking submit button with empty fields to make the errors appear
@@ -222,7 +226,7 @@ describe('LoginPage', () => {
// ******** test form buttons and links ******** // ******** test form buttons and links ********
it('should match default button state', () => { it('should match default button state', () => {
render(reduxWrapper(<LoginPage {...props} />)); render(reduxWrapper(<IntlLoginPage {...props} />));
expect(screen.getByText('Sign in')).toBeDefined(); expect(screen.getByText('Sign in')).toBeDefined();
}); });
@@ -235,7 +239,7 @@ describe('LoginPage', () => {
}, },
}); });
render(reduxWrapper(<LoginPage {...props} />)); render(reduxWrapper(<IntlLoginPage {...props} />));
expect(screen.getByText( expect(screen.getByText(
'pending', 'pending',
@@ -243,7 +247,7 @@ describe('LoginPage', () => {
}); });
it('should show forgot password link', () => { it('should show forgot password link', () => {
render(reduxWrapper(<LoginPage {...props} />)); render(reduxWrapper(<IntlLoginPage {...props} />));
expect(screen.getByText( expect(screen.getByText(
'Forgot password', 'Forgot password',
@@ -263,7 +267,7 @@ describe('LoginPage', () => {
}, },
}); });
render(reduxWrapper(<LoginPage {...props} />)); render(reduxWrapper(<IntlLoginPage {...props} />));
expect(screen.getByText( expect(screen.getByText(
'', '',
{ selector: `#${ssoProvider.id}` }, { selector: `#${ssoProvider.id}` },
@@ -285,7 +289,7 @@ describe('LoginPage', () => {
}, },
}); });
const { queryByText } = render(reduxWrapper(<LoginPage {...props} />)); const { queryByText } = render(reduxWrapper(<IntlLoginPage {...props} />));
expect(queryByText('Company or school credentials')).toBeNull(); expect(queryByText('Company or school credentials')).toBeNull();
expect(queryByText('Or sign in with:')).toBeNull(); expect(queryByText('Or sign in with:')).toBeNull();
expect(queryByText('Institution/campus credentials')).toBeNull(); expect(queryByText('Institution/campus credentials')).toBeNull();
@@ -305,7 +309,7 @@ describe('LoginPage', () => {
}, },
}); });
const { queryByText } = render(reduxWrapper(<LoginPage {...props} />)); const { queryByText } = render(reduxWrapper(<IntlLoginPage {...props} />));
expect(queryByText('Company or school credentials')).toBeNull(); expect(queryByText('Company or school credentials')).toBeNull();
expect(queryByText('Or sign in with:')).toBeNull(); expect(queryByText('Or sign in with:')).toBeNull();
}); });
@@ -325,7 +329,7 @@ describe('LoginPage', () => {
}, },
}); });
const { queryByText } = render(reduxWrapper(<LoginPage {...props} />)); const { queryByText } = render(reduxWrapper(<IntlLoginPage {...props} />));
expect(queryByText('Or sign in with:')).toBeDefined(); expect(queryByText('Or sign in with:')).toBeDefined();
expect(queryByText('Company or school credentials')).toBeDefined(); expect(queryByText('Company or school credentials')).toBeDefined();
expect(queryByText('Institution/campus credentials')).toBeDefined(); expect(queryByText('Institution/campus credentials')).toBeDefined();
@@ -350,7 +354,7 @@ describe('LoginPage', () => {
}, },
}); });
const { queryByText } = render(reduxWrapper(<LoginPage {...props} />)); const { queryByText } = render(reduxWrapper(<IntlLoginPage {...props} />));
expect(queryByText('Or sign in with:')).toBeDefined(); expect(queryByText('Or sign in with:')).toBeDefined();
expect(queryByText('Company or school credentials')).toBeNull(); expect(queryByText('Company or school credentials')).toBeNull();
expect(queryByText('Institution/campus credentials')).toBeDefined(); expect(queryByText('Institution/campus credentials')).toBeDefined();
@@ -378,7 +382,7 @@ describe('LoginPage', () => {
}, },
}); });
const { queryByText } = render(reduxWrapper(<LoginPage {...props} />)); const { queryByText } = render(reduxWrapper(<IntlLoginPage {...props} />));
expect(queryByText('Or sign in with:')).toBeDefined(); expect(queryByText('Or sign in with:')).toBeDefined();
expect(queryByText('Institution/campus credentials')).toBeDefined(); expect(queryByText('Institution/campus credentials')).toBeDefined();
@@ -398,7 +402,7 @@ describe('LoginPage', () => {
}, },
}); });
const { queryByText } = render(reduxWrapper(<LoginPage {...props} />)); const { queryByText } = render(reduxWrapper(<IntlLoginPage {...props} />));
expect(queryByText('Or sign in with:')).toBeNull(); expect(queryByText('Or sign in with:')).toBeNull();
expect(queryByText('Institution/campus credentials')).toBeNull(); expect(queryByText('Institution/campus credentials')).toBeNull();
expect(queryByText('Company or school credentials')).toBeNull(); expect(queryByText('Company or school credentials')).toBeNull();
@@ -416,7 +420,7 @@ describe('LoginPage', () => {
}, },
}); });
const { queryByText } = render(reduxWrapper(<LoginPage {...props} />)); const { queryByText } = render(reduxWrapper(<IntlLoginPage {...props} />));
expect(queryByText('Or sign in with:')).toBeDefined(); expect(queryByText('Or sign in with:')).toBeDefined();
expect(queryByText('Company or school credentials')).toBeNull(); expect(queryByText('Company or school credentials')).toBeNull();
expect(queryByText('Institution/campus credentials')).toBeDefined(); expect(queryByText('Institution/campus credentials')).toBeDefined();
@@ -439,7 +443,7 @@ describe('LoginPage', () => {
}, },
}); });
render(reduxWrapper(<LoginPage {...props} />)); render(reduxWrapper(<IntlLoginPage {...props} />));
expect(screen.getByText( expect(screen.getByText(
'', '',
{ selector: '#login-failure-alert' }, { selector: '#login-failure-alert' },
@@ -463,7 +467,7 @@ describe('LoginPage', () => {
+ 'linked '}${ getConfig().SITE_NAME } account. To link your accounts, sign in now using your ${ + 'linked '}${ getConfig().SITE_NAME } account. To link your accounts, sign in now using your ${
getConfig().SITE_NAME } password.`; getConfig().SITE_NAME } password.`;
render(reduxWrapper(<LoginPage {...props} />)); render(reduxWrapper(<IntlLoginPage {...props} />));
expect(screen.getByText( expect(screen.getByText(
'', '',
{ selector: '#tpa-alert' }, { selector: '#tpa-alert' },
@@ -482,7 +486,7 @@ describe('LoginPage', () => {
}, },
}, },
}); });
render(reduxWrapper(<LoginPage {...props} />)); render(reduxWrapper(<IntlLoginPage {...props} />));
expect(screen.getByText( expect(screen.getByText(
'', '',
{ selector: '#login-failure-alert' }, { selector: '#login-failure-alert' },
@@ -499,7 +503,7 @@ describe('LoginPage', () => {
}, },
}); });
render(reduxWrapper(<LoginPage {...props} />)); render(reduxWrapper(<IntlLoginPage {...props} />));
expect(screen.getByText( expect(screen.getByText(
'', '',
{ selector: '#login-failure-alert' }, { selector: '#login-failure-alert' },
@@ -523,7 +527,7 @@ describe('LoginPage', () => {
delete window.location; delete window.location;
window.location = { href: getConfig().BASE_URL }; window.location = { href: getConfig().BASE_URL };
render(reduxWrapper(<LoginPage {...props} />)); render(reduxWrapper(<IntlLoginPage {...props} />));
expect(window.location.href).toBe(dashboardURL); expect(window.location.href).toBe(dashboardURL);
}); });
@@ -550,7 +554,7 @@ describe('LoginPage', () => {
delete window.location; delete window.location;
window.location = { href: getConfig().BASE_URL }; window.location = { href: getConfig().BASE_URL };
render(reduxWrapper(<LoginPage {...props} />)); render(reduxWrapper(<IntlLoginPage {...props} />));
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + authCompleteUrl); expect(window.location.href).toBe(getConfig().LMS_BASE_URL + authCompleteUrl);
}); });
@@ -569,7 +573,7 @@ describe('LoginPage', () => {
delete window.location; delete window.location;
window.location = { href: getConfig().BASE_URL }; window.location = { href: getConfig().BASE_URL };
render(reduxWrapper(<LoginPage {...props} />)); render(reduxWrapper(<IntlLoginPage {...props} />));
fireEvent.click(screen.getByText( fireEvent.click(screen.getByText(
'', '',
@@ -598,7 +602,7 @@ describe('LoginPage', () => {
delete window.location; delete window.location;
window.location = { href: getConfig().BASE_URL }; window.location = { href: getConfig().BASE_URL };
render(reduxWrapper(<LoginPage {...props} />)); render(reduxWrapper(<IntlLoginPage {...props} />));
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + finishAuthUrl); expect(window.location.href).toBe(getConfig().LMS_BASE_URL + finishAuthUrl);
}); });
@@ -620,7 +624,7 @@ describe('LoginPage', () => {
delete window.location; delete window.location;
window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: `?next=/dashboard&tpa_hint=${ssoProvider.id}` }; window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: `?next=/dashboard&tpa_hint=${ssoProvider.id}` };
render(reduxWrapper(<LoginPage {...props} />)); render(reduxWrapper(<IntlLoginPage {...props} />));
expect(screen.getByText( expect(screen.getByText(
'', '',
{ selector: `#${ssoProvider.id}` }, { selector: `#${ssoProvider.id}` },
@@ -647,7 +651,7 @@ describe('LoginPage', () => {
delete window.location; delete window.location;
window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: `?next=/dashboard&tpa_hint=${ssoProvider.id}` }; window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: `?next=/dashboard&tpa_hint=${ssoProvider.id}` };
const { container } = render(reduxWrapper(<LoginPage {...props} />)); const { container } = render(reduxWrapper(<IntlLoginPage {...props} />));
expect(container.querySelector('.react-loading-skeleton')).toBeTruthy(); expect(container.querySelector('.react-loading-skeleton')).toBeTruthy();
}); });
@@ -669,7 +673,7 @@ describe('LoginPage', () => {
window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: `?next=/dashboard&tpa_hint=${secondaryProviders.id}` }; window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: `?next=/dashboard&tpa_hint=${secondaryProviders.id}` };
secondaryProviders.iconImage = null; secondaryProviders.iconImage = null;
render(reduxWrapper(<LoginPage {...props} />)); render(reduxWrapper(<IntlLoginPage {...props} />));
expect(window.location.href).toEqual(getConfig().LMS_BASE_URL + secondaryProviders.loginUrl); expect(window.location.href).toEqual(getConfig().LMS_BASE_URL + secondaryProviders.loginUrl);
}); });
@@ -689,7 +693,7 @@ describe('LoginPage', () => {
delete window.location; delete window.location;
window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: '?next=/dashboard&tpa_hint=invalid' }; window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: '?next=/dashboard&tpa_hint=invalid' };
const { container } = render(reduxWrapper(<LoginPage {...props} />)); const { container } = render(reduxWrapper(<IntlLoginPage {...props} />));
expect(container.querySelector(`#${ssoProvider.id}`).querySelector('#provider-name').textContent).toEqual(`${ssoProvider.name}`); expect(container.querySelector(`#${ssoProvider.id}`).querySelector('#provider-name').textContent).toEqual(`${ssoProvider.name}`);
mergeConfig({ mergeConfig({
@@ -713,7 +717,7 @@ describe('LoginPage', () => {
delete window.location; delete window.location;
window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: `?tpa_hint=${ssoProvider.id}` }; window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: `?tpa_hint=${ssoProvider.id}` };
render(reduxWrapper(<LoginPage {...props} />)); render(reduxWrapper(<IntlLoginPage {...props} />));
expect(screen.getByText( expect(screen.getByText(
'Show me other ways to sign in or register', 'Show me other ways to sign in or register',
).textContent).toBeDefined(); ).textContent).toBeDefined();
@@ -739,7 +743,7 @@ describe('LoginPage', () => {
delete window.location; delete window.location;
window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: `?tpa_hint=${ssoProvider.id}` }; window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: `?tpa_hint=${ssoProvider.id}` };
render(reduxWrapper(<LoginPage {...props} />)); render(reduxWrapper(<IntlLoginPage {...props} />));
expect(screen.getByText( expect(screen.getByText(
'Show me other ways to sign in', 'Show me other ways to sign in',
).textContent).toBeDefined(); ).textContent).toBeDefined();
@@ -748,8 +752,8 @@ describe('LoginPage', () => {
// ******** miscellaneous tests ******** // ******** miscellaneous tests ********
it('should send page event when login page is rendered', () => { it('should send page event when login page is rendered', () => {
render(reduxWrapper(<LoginPage {...props} />)); render(reduxWrapper(<IntlLoginPage {...props} />));
expect(sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'login'); expect(sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'login', { app_name: APP_NAME });
}); });
it('tests that form is in invalid state when it is submitted', () => { it('tests that form is in invalid state when it is submitted', () => {
@@ -762,7 +766,7 @@ describe('LoginPage', () => {
}); });
store.dispatch = jest.fn(store.dispatch); store.dispatch = jest.fn(store.dispatch);
render(reduxWrapper(<LoginPage {...props} />)); render(reduxWrapper(<IntlLoginPage {...props} />));
expect(store.dispatch).toHaveBeenCalledWith(backupLoginFormBegin( expect(store.dispatch).toHaveBeenCalledWith(backupLoginFormBegin(
{ {
formFields: { formFields: {
@@ -776,13 +780,13 @@ describe('LoginPage', () => {
}); });
it('should send track event when forgot password link is clicked', () => { it('should send track event when forgot password link is clicked', () => {
render(reduxWrapper(<LoginPage {...props} />)); render(reduxWrapper(<IntlLoginPage {...props} />));
fireEvent.click(screen.getByText( fireEvent.click(screen.getByText(
'Forgot password', 'Forgot password',
{ selector: '#forgot-password' }, { selector: '#forgot-password' },
)); ));
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.password-reset_form.toggled', { category: 'user-engagement' }); expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.password-reset_form.toggled', { category: 'user-engagement', app_name: APP_NAME });
}); });
it('should backup the login form state when shouldBackupState is true', () => { it('should backup the login form state when shouldBackupState is true', () => {
@@ -795,7 +799,7 @@ describe('LoginPage', () => {
}); });
store.dispatch = jest.fn(store.dispatch); store.dispatch = jest.fn(store.dispatch);
render(reduxWrapper(<LoginPage {...props} />)); render(reduxWrapper(<IntlLoginPage {...props} />));
expect(store.dispatch).toHaveBeenCalledWith(backupLoginFormBegin( expect(store.dispatch).toHaveBeenCalledWith(backupLoginFormBegin(
{ {
formFields: { formFields: {
@@ -824,7 +828,7 @@ describe('LoginPage', () => {
}, },
}); });
const { container } = render(reduxWrapper(<LoginPage {...props} />)); const { container } = render(reduxWrapper(<IntlLoginPage {...props} />));
expect(container.querySelector('input#emailOrUsername').value).toEqual('john_doe'); expect(container.querySelector('input#emailOrUsername').value).toEqual('john_doe');
expect(container.querySelector('input#password').value).toEqual('test-password'); expect(container.querySelector('input#password').value).toEqual('test-password');
}); });

View File

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

View File

@@ -1,8 +1,9 @@
import React from 'react';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { getConfig, mergeConfig } from '@edx/frontend-platform'; import { getConfig, mergeConfig } from '@edx/frontend-platform';
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics'; import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
import { configure, IntlProvider } from '@edx/frontend-platform/i18n'; import { configure, injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import { fireEvent, render, screen } from '@testing-library/react'; import { fireEvent, render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
import configureStore from 'redux-mock-store'; import configureStore from 'redux-mock-store';
@@ -10,18 +11,24 @@ import configureStore from 'redux-mock-store';
import Logistration from './Logistration'; import Logistration from './Logistration';
import { clearThirdPartyAuthContextErrorMessage } from '../common-components/data/actions'; import { clearThirdPartyAuthContextErrorMessage } from '../common-components/data/actions';
import { import {
APP_NAME,
COMPLETE_STATE, LOGIN_PAGE, REGISTER_PAGE, COMPLETE_STATE, LOGIN_PAGE, REGISTER_PAGE,
} from '../data/constants'; } from '../data/constants';
import { backupLoginForm } from '../login/data/actions'; import { backupLoginForm } from '../login/data/actions';
import { backupRegistrationForm } from '../register/data/actions'; import { backupRegistrationForm } from '../register/data/actions';
import { NOT_INITIALIZED } from '../register/data/optimizelyExperiment/helper';
import useAutoGeneratedUsernameExperimentVariation
from '../register/data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation';
jest.mock('@edx/frontend-platform/analytics', () => ({ jest.mock('@edx/frontend-platform/analytics', () => ({
sendPageEvent: jest.fn(), sendPageEvent: jest.fn(),
sendTrackEvent: jest.fn(), sendTrackEvent: jest.fn(),
})); }));
jest.mock('@edx/frontend-platform/auth'); jest.mock('@edx/frontend-platform/auth');
jest.mock('../register/data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation', () => jest.fn());
const mockStore = configureStore(); const mockStore = configureStore();
const IntlLogistration = injectIntl(Logistration);
describe('Logistration', () => { describe('Logistration', () => {
let store = {}; let store = {};
@@ -82,6 +89,7 @@ describe('Logistration', () => {
})), })),
})); }));
useAutoGeneratedUsernameExperimentVariation.mockReturnValue(NOT_INITIALIZED);
configure({ configure({
loggingService: { logError: jest.fn() }, loggingService: { logError: jest.fn() },
config: { config: {
@@ -93,7 +101,7 @@ describe('Logistration', () => {
}); });
it('should do nothing when user clicks on the same tab (login/register) again', () => { it('should do nothing when user clicks on the same tab (login/register) again', () => {
const { container } = render(reduxWrapper(<Logistration />)); const { container } = render(reduxWrapper(<IntlLogistration />));
// While staying on the registration form, clicking the register tab again // While staying on the registration form, clicking the register tab again
fireEvent.click(container.querySelector('a[data-rb-event-key="/register"]')); fireEvent.click(container.querySelector('a[data-rb-event-key="/register"]'));
@@ -105,14 +113,14 @@ describe('Logistration', () => {
ALLOW_PUBLIC_ACCOUNT_CREATION: true, ALLOW_PUBLIC_ACCOUNT_CREATION: true,
}); });
const { container } = render(reduxWrapper(<Logistration />)); const { container } = render(reduxWrapper(<IntlLogistration />));
expect(container.querySelector('RegistrationPage')).toBeDefined(); expect(container.querySelector('RegistrationPage')).toBeDefined();
}); });
it('should render login page', () => { it('should render login page', () => {
const props = { selectedPage: LOGIN_PAGE }; const props = { selectedPage: LOGIN_PAGE };
const { container } = render(reduxWrapper(<Logistration {...props} />)); const { container } = render(reduxWrapper(<IntlLogistration {...props} />));
expect(container.querySelector('LoginPage')).toBeDefined(); expect(container.querySelector('LoginPage')).toBeDefined();
}); });
@@ -123,7 +131,7 @@ describe('Logistration', () => {
}); });
let props = { selectedPage: LOGIN_PAGE }; let props = { selectedPage: LOGIN_PAGE };
const { rerender } = render(reduxWrapper(<Logistration {...props} />)); const { rerender } = render(reduxWrapper(<IntlLogistration {...props} />));
// verifying sign in heading // verifying sign in heading
expect(screen.getByRole('heading', { level: 3 }).textContent).toEqual('Sign in'); expect(screen.getByRole('heading', { level: 3 }).textContent).toEqual('Sign in');
@@ -131,7 +139,7 @@ describe('Logistration', () => {
// register page is still accessible when SHOW_REGISTRATION_LINKS is false // register page is still accessible when SHOW_REGISTRATION_LINKS is false
// but it needs to be accessed directly // but it needs to be accessed directly
props = { selectedPage: REGISTER_PAGE }; props = { selectedPage: REGISTER_PAGE };
rerender(reduxWrapper(<Logistration {...props} />)); rerender(reduxWrapper(<IntlLogistration {...props} />));
// verifying register heading // verifying register heading
expect(screen.getByRole('heading', { level: 3 }).textContent).toEqual('Register'); expect(screen.getByRole('heading', { level: 3 }).textContent).toEqual('Register');
@@ -158,7 +166,7 @@ describe('Logistration', () => {
}); });
const props = { selectedPage: LOGIN_PAGE }; const props = { selectedPage: LOGIN_PAGE };
const { container } = render(reduxWrapper(<Logistration {...props} />)); const { container } = render(reduxWrapper(<IntlLogistration {...props} />));
// verifying sign in heading for institution login false // verifying sign in heading for institution login false
expect(screen.getByRole('heading', { level: 3 }).textContent).toEqual('Sign in'); expect(screen.getByRole('heading', { level: 3 }).textContent).toEqual('Sign in');
@@ -188,7 +196,7 @@ describe('Logistration', () => {
}); });
const props = { selectedPage: LOGIN_PAGE }; const props = { selectedPage: LOGIN_PAGE };
render(reduxWrapper(<Logistration {...props} />)); render(reduxWrapper(<IntlLogistration {...props} />));
expect(screen.getByText('Institution/campus credentials')).toBeDefined(); expect(screen.getByText('Institution/campus credentials')).toBeDefined();
// on clicking "Institution/campus credentials" button, it should display institution login page // on clicking "Institution/campus credentials" button, it should display institution login page
@@ -219,11 +227,11 @@ describe('Logistration', () => {
}); });
const props = { selectedPage: LOGIN_PAGE }; const props = { selectedPage: LOGIN_PAGE };
render(reduxWrapper(<Logistration {...props} />)); render(reduxWrapper(<IntlLogistration {...props} />));
fireEvent.click(screen.getByText('Institution/campus credentials')); fireEvent.click(screen.getByText('Institution/campus credentials'));
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.institution_login_form.toggled', { category: 'user-engagement' }); expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.institution_login_form.toggled', { category: 'user-engagement', app_name: APP_NAME });
expect(sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'institution_login'); expect(sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'institution_login', { app_name: APP_NAME });
mergeConfig({ mergeConfig({
DISABLE_ENTERPRISE_LOGIN: '', DISABLE_ENTERPRISE_LOGIN: '',
@@ -251,7 +259,7 @@ describe('Logistration', () => {
delete window.location; delete window.location;
window.location = { hostname: getConfig().SITE_NAME, href: getConfig().BASE_URL }; window.location = { hostname: getConfig().SITE_NAME, href: getConfig().BASE_URL };
render(reduxWrapper(<Logistration />)); render(reduxWrapper(<IntlLogistration />));
fireEvent.click(screen.getByText('Institution/campus credentials')); fireEvent.click(screen.getByText('Institution/campus credentials'));
expect(screen.getByText('Test University')).toBeDefined(); expect(screen.getByText('Test University')).toBeDefined();
@@ -262,7 +270,7 @@ describe('Logistration', () => {
it('should fire action to backup registration form on tab click', () => { it('should fire action to backup registration form on tab click', () => {
store.dispatch = jest.fn(store.dispatch); store.dispatch = jest.fn(store.dispatch);
const { container } = render(reduxWrapper(<Logistration />)); const { container } = render(reduxWrapper(<IntlLogistration />));
fireEvent.click(container.querySelector('a[data-rb-event-key="/login"]')); fireEvent.click(container.querySelector('a[data-rb-event-key="/login"]'));
expect(store.dispatch).toHaveBeenCalledWith(backupRegistrationForm()); expect(store.dispatch).toHaveBeenCalledWith(backupRegistrationForm());
}); });
@@ -270,14 +278,14 @@ describe('Logistration', () => {
it('should fire action to backup login form on tab click', () => { it('should fire action to backup login form on tab click', () => {
store.dispatch = jest.fn(store.dispatch); store.dispatch = jest.fn(store.dispatch);
const props = { selectedPage: LOGIN_PAGE }; const props = { selectedPage: LOGIN_PAGE };
const { container } = render(reduxWrapper(<Logistration {...props} />)); const { container } = render(reduxWrapper(<IntlLogistration {...props} />));
fireEvent.click(container.querySelector('a[data-rb-event-key="/register"]')); fireEvent.click(container.querySelector('a[data-rb-event-key="/register"]'));
expect(store.dispatch).toHaveBeenCalledWith(backupLoginForm()); expect(store.dispatch).toHaveBeenCalledWith(backupLoginForm());
}); });
it('should clear tpa context errorMessage tab click', () => { it('should clear tpa context errorMessage tab click', () => {
store.dispatch = jest.fn(store.dispatch); store.dispatch = jest.fn(store.dispatch);
const { container } = render(reduxWrapper(<Logistration />)); const { container } = render(reduxWrapper(<IntlLogistration />));
fireEvent.click(container.querySelector('a[data-rb-event-key="/login"]')); fireEvent.click(container.querySelector('a[data-rb-event-key="/login"]'));
expect(store.dispatch).toHaveBeenCalledWith(clearThirdPartyAuthContextErrorMessage()); expect(store.dispatch).toHaveBeenCalledWith(clearThirdPartyAuthContextErrorMessage());
}); });

View File

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

View File

@@ -1,9 +1,10 @@
import React from 'react';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { getConfig, mergeConfig } from '@edx/frontend-platform'; import { getConfig, mergeConfig } from '@edx/frontend-platform';
import { identifyAuthenticatedUser, sendTrackEvent } from '@edx/frontend-platform/analytics'; import { identifyAuthenticatedUser, sendTrackEvent } from '@edx/frontend-platform/analytics';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth'; import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { configure, IntlProvider } from '@edx/frontend-platform/i18n'; import { configure, injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import { import {
fireEvent, render, screen, fireEvent, render, screen,
} from '@testing-library/react'; } from '@testing-library/react';
@@ -11,6 +12,7 @@ import { MemoryRouter, mockNavigate, useLocation } from 'react-router-dom';
import configureStore from 'redux-mock-store'; import configureStore from 'redux-mock-store';
import { import {
APP_NAME,
AUTHN_PROGRESSIVE_PROFILING, AUTHN_PROGRESSIVE_PROFILING,
COMPLETE_STATE, DEFAULT_REDIRECT_URL, COMPLETE_STATE, DEFAULT_REDIRECT_URL,
EMBEDDED, EMBEDDED,
@@ -21,6 +23,7 @@ import {
import { saveUserProfile } from '../data/actions'; import { saveUserProfile } from '../data/actions';
import ProgressiveProfiling from '../ProgressiveProfiling'; import ProgressiveProfiling from '../ProgressiveProfiling';
const IntlProgressiveProfilingPage = injectIntl(ProgressiveProfiling);
const mockStore = configureStore(); const mockStore = configureStore();
jest.mock('@edx/frontend-platform/analytics', () => ({ jest.mock('@edx/frontend-platform/analytics', () => ({
@@ -112,7 +115,7 @@ describe('ProgressiveProfilingTests', () => {
mergeConfig({ mergeConfig({
AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK: '', AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK: '',
}); });
const { queryByRole } = render(reduxWrapper(<ProgressiveProfiling />)); const { queryByRole } = render(reduxWrapper(<IntlProgressiveProfilingPage />));
const button = queryByRole('button', { name: /learn more about how we use this information/i }); const button = queryByRole('button', { name: /learn more about how we use this information/i });
expect(button).toBeNull(); expect(button).toBeNull();
@@ -123,7 +126,7 @@ describe('ProgressiveProfilingTests', () => {
AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK: 'http://localhost:1999/support', AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK: 'http://localhost:1999/support',
}); });
const { getByText } = render(reduxWrapper(<ProgressiveProfiling />)); const { getByText } = render(reduxWrapper(<IntlProgressiveProfilingPage />));
const learnMoreButton = getByText('Learn more about how we use this information.'); const learnMoreButton = getByText('Learn more about how we use this information.');
@@ -133,7 +136,7 @@ describe('ProgressiveProfilingTests', () => {
it('should open modal on pressing skip for now button', () => { it('should open modal on pressing skip for now button', () => {
delete window.location; delete window.location;
window.location = { href: getConfig().BASE_URL.concat(AUTHN_PROGRESSIVE_PROFILING) }; window.location = { href: getConfig().BASE_URL.concat(AUTHN_PROGRESSIVE_PROFILING) };
const { getByRole } = render(reduxWrapper(<ProgressiveProfiling />)); const { getByRole } = render(reduxWrapper(<IntlProgressiveProfilingPage />));
const skipButton = getByRole('button', { name: /skip for now/i }); const skipButton = getByRole('button', { name: /skip for now/i });
fireEvent.click(skipButton); fireEvent.click(skipButton);
@@ -141,14 +144,15 @@ describe('ProgressiveProfilingTests', () => {
const modalContentContainer = document.getElementsByClassName('.pgn__modal-content-container'); const modalContentContainer = document.getElementsByClassName('.pgn__modal-content-container');
expect(modalContentContainer).toBeTruthy(); expect(modalContentContainer).toBeTruthy();
const payload = { host: '', app_name: APP_NAME };
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.welcome.page.skip.link.clicked', { host: '' }); expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.welcome.page.skip.link.clicked', payload);
}); });
// ******** test event functionality ******** // ******** test event functionality ********
it('should make identify call to segment on progressive profiling page', () => { it('should make identify call to segment on progressive profiling page', () => {
render(reduxWrapper(<ProgressiveProfiling />)); render(reduxWrapper(<IntlProgressiveProfilingPage />));
expect(identifyAuthenticatedUser).toHaveBeenCalledWith(3); expect(identifyAuthenticatedUser).toHaveBeenCalledWith(3);
expect(identifyAuthenticatedUser).toHaveBeenCalled(); expect(identifyAuthenticatedUser).toHaveBeenCalled();
@@ -158,12 +162,12 @@ describe('ProgressiveProfilingTests', () => {
mergeConfig({ mergeConfig({
AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK: 'http://localhost:1999/support', AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK: 'http://localhost:1999/support',
}); });
render(reduxWrapper(<ProgressiveProfiling />)); render(reduxWrapper(<IntlProgressiveProfilingPage />));
const supportLink = screen.getByRole('link', { name: /learn more about how we use this information/i }); const supportLink = screen.getByRole('link', { name: /learn more about how we use this information/i });
fireEvent.click(supportLink); fireEvent.click(supportLink);
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.welcome.page.support.link.clicked'); expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.welcome.page.support.link.clicked', { app_name: APP_NAME });
}); });
it('should set empty host property value for non-embedded experience', () => { it('should set empty host property value for non-embedded experience', () => {
@@ -173,10 +177,11 @@ describe('ProgressiveProfilingTests', () => {
isLevelOfEducationSelected: false, isLevelOfEducationSelected: false,
isWorkExperienceSelected: false, isWorkExperienceSelected: false,
host: '', host: '',
app_name: APP_NAME,
}; };
delete window.location; delete window.location;
window.location = { href: getConfig().BASE_URL.concat(AUTHN_PROGRESSIVE_PROFILING) }; window.location = { href: getConfig().BASE_URL.concat(AUTHN_PROGRESSIVE_PROFILING) };
render(reduxWrapper(<ProgressiveProfiling />)); render(reduxWrapper(<IntlProgressiveProfilingPage />));
const nextButton = screen.getByText('Next'); const nextButton = screen.getByText('Next');
fireEvent.click(nextButton); fireEvent.click(nextButton);
@@ -192,7 +197,7 @@ describe('ProgressiveProfilingTests', () => {
extended_profile: [{ field_name: 'company', field_value: 'test company' }], extended_profile: [{ field_name: 'company', field_value: 'test company' }],
}; };
store.dispatch = jest.fn(store.dispatch); store.dispatch = jest.fn(store.dispatch);
const { getByLabelText, getByText } = render(reduxWrapper(<ProgressiveProfiling />)); const { getByLabelText, getByText } = render(reduxWrapper(<IntlProgressiveProfilingPage />));
const genderSelect = getByLabelText('Gender'); const genderSelect = getByLabelText('Gender');
const companyInput = getByLabelText('Company'); const companyInput = getByLabelText('Company');
@@ -214,7 +219,7 @@ describe('ProgressiveProfilingTests', () => {
}, },
}); });
const { container } = render(reduxWrapper(<ProgressiveProfiling />)); const { container } = render(reduxWrapper(<IntlProgressiveProfilingPage />));
const errorElement = container.querySelector('#pp-page-errors'); const errorElement = container.querySelector('#pp-page-errors');
expect(errorElement).toBeTruthy(); expect(errorElement).toBeTruthy();
@@ -230,7 +235,7 @@ describe('ProgressiveProfilingTests', () => {
href: getConfig().BASE_URL, href: getConfig().BASE_URL,
}; };
render(reduxWrapper(<ProgressiveProfiling />)); render(reduxWrapper(<IntlProgressiveProfilingPage />));
expect(window.location.href).toEqual(DASHBOARD_URL); expect(window.location.href).toEqual(DASHBOARD_URL);
}); });
@@ -248,7 +253,7 @@ describe('ProgressiveProfilingTests', () => {
success: true, success: true,
}, },
}); });
const { container } = render(reduxWrapper(<ProgressiveProfiling />)); const { container } = render(reduxWrapper(<IntlProgressiveProfilingPage />));
const nextButton = container.querySelector('button.btn-brand'); const nextButton = container.querySelector('button.btn-brand');
expect(nextButton.textContent).toEqual('Next'); expect(nextButton.textContent).toEqual('Next');
@@ -275,7 +280,7 @@ describe('ProgressiveProfilingTests', () => {
}, },
}); });
const { container } = render(reduxWrapper(<ProgressiveProfiling />)); const { container } = render(reduxWrapper(<IntlProgressiveProfilingPage />));
const nextButton = container.querySelector('button.btn-brand'); const nextButton = container.querySelector('button.btn-brand');
expect(nextButton.textContent).toEqual('Submit'); expect(nextButton.textContent).toEqual('Submit');
@@ -309,12 +314,12 @@ describe('ProgressiveProfilingTests', () => {
href: getConfig().BASE_URL.concat(AUTHN_PROGRESSIVE_PROFILING), href: getConfig().BASE_URL.concat(AUTHN_PROGRESSIVE_PROFILING),
search: `?host=${host}&variant=${EMBEDDED}`, search: `?host=${host}&variant=${EMBEDDED}`,
}; };
render(reduxWrapper(<ProgressiveProfiling />)); render(reduxWrapper(<IntlProgressiveProfilingPage />));
const skipLinkButton = screen.getByText('Skip for now'); const skipLinkButton = screen.getByText('Skip for now');
fireEvent.click(skipLinkButton); fireEvent.click(skipLinkButton);
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.welcome.page.skip.link.clicked', { host }); expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.welcome.page.skip.link.clicked', { host, app_name: APP_NAME });
}); });
it('should show spinner while fetching the optional fields', () => { it('should show spinner while fetching the optional fields', () => {
@@ -334,7 +339,7 @@ describe('ProgressiveProfilingTests', () => {
}, },
}); });
const { container } = render(reduxWrapper(<ProgressiveProfiling />)); const { container } = render(reduxWrapper(<IntlProgressiveProfilingPage />));
const tpaSpinnerElement = container.querySelector('#tpa-spinner'); const tpaSpinnerElement = container.querySelector('#tpa-spinner');
expect(tpaSpinnerElement).toBeTruthy(); expect(tpaSpinnerElement).toBeTruthy();
@@ -347,13 +352,14 @@ describe('ProgressiveProfilingTests', () => {
isLevelOfEducationSelected: false, isLevelOfEducationSelected: false,
isWorkExperienceSelected: false, isWorkExperienceSelected: false,
host: 'http://example.com', host: 'http://example.com',
app_name: APP_NAME,
}; };
delete window.location; delete window.location;
window.location = { window.location = {
href: getConfig().BASE_URL.concat(AUTHN_PROGRESSIVE_PROFILING), href: getConfig().BASE_URL.concat(AUTHN_PROGRESSIVE_PROFILING),
search: `?host=${host}`, search: `?host=${host}`,
}; };
render(reduxWrapper(<ProgressiveProfiling />)); render(reduxWrapper(<IntlProgressiveProfilingPage />));
const submitButton = screen.getByText('Next'); const submitButton = screen.getByText('Next');
fireEvent.click(submitButton); fireEvent.click(submitButton);
@@ -368,7 +374,7 @@ describe('ProgressiveProfilingTests', () => {
search: `?variant=${EMBEDDED}&host=${host}`, search: `?variant=${EMBEDDED}&host=${host}`,
}; };
const { container } = render(reduxWrapper(<ProgressiveProfiling />)); const { container } = render(reduxWrapper(<IntlProgressiveProfilingPage />));
const genderField = container.querySelector('#gender'); const genderField = container.querySelector('#gender');
expect(genderField).toBeTruthy(); expect(genderField).toBeTruthy();
@@ -389,7 +395,7 @@ describe('ProgressiveProfilingTests', () => {
}, },
}); });
render(reduxWrapper(<ProgressiveProfiling />)); render(reduxWrapper(<IntlProgressiveProfilingPage />));
expect(window.location.href).toBe(DASHBOARD_URL); expect(window.location.href).toBe(DASHBOARD_URL);
}); });
@@ -417,7 +423,7 @@ describe('ProgressiveProfilingTests', () => {
}, },
}); });
render(reduxWrapper(<ProgressiveProfiling />)); render(reduxWrapper(<IntlProgressiveProfilingPage />));
const submitButton = screen.getByText('Submit'); const submitButton = screen.getByText('Submit');
fireEvent.click(submitButton); fireEvent.click(submitButton);
expect(window.location.href).toBe(redirectUrl); expect(window.location.href).toBe(redirectUrl);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,7 +4,6 @@ import React, {
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { getConfig } from '@edx/frontend-platform'; import { getConfig } from '@edx/frontend-platform';
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
import { useIntl } from '@edx/frontend-platform/i18n'; import { useIntl } from '@edx/frontend-platform/i18n';
import { Form, Spinner, StatefulButton } from '@openedx/paragon'; import { Form, Spinner, StatefulButton } from '@openedx/paragon';
import classNames from 'classnames'; import classNames from 'classnames';
@@ -18,6 +17,7 @@ import {
backupRegistrationFormBegin, backupRegistrationFormBegin,
clearRegistrationBackendError, clearRegistrationBackendError,
registerNewUser, registerNewUser,
setAutoGeneratedUsernameExperimentData,
setEmailSuggestionInStore, setEmailSuggestionInStore,
setUserPipelineDataLoaded, setUserPipelineDataLoaded,
} from './data/actions'; } from './data/actions';
@@ -25,6 +25,8 @@ import {
FORM_SUBMISSION_ERROR, FORM_SUBMISSION_ERROR,
TPA_AUTHENTICATION_FAILURE, TPA_AUTHENTICATION_FAILURE,
} from './data/constants'; } from './data/constants';
import { AUTO_GENERATED_USERNAME_REGISTRATION_EXP_VARIATION, NOT_INITIALIZED } from './data/optimizelyExperiment/helper';
import useAutoGeneratedUsernameExperimentVariation from './data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation';
import getBackendValidations from './data/selectors'; import getBackendValidations from './data/selectors';
import { import {
isFormValid, prepareRegistrationPayload, isFormValid, prepareRegistrationPayload,
@@ -41,11 +43,12 @@ import { getThirdPartyAuthContext as getRegistrationDataFromBackend } from '../c
import EnterpriseSSO from '../common-components/EnterpriseSSO'; import EnterpriseSSO from '../common-components/EnterpriseSSO';
import ThirdPartyAuth from '../common-components/ThirdPartyAuth'; import ThirdPartyAuth from '../common-components/ThirdPartyAuth';
import { import {
COMPLETE_STATE, PENDING_STATE, REGISTER_PAGE, APP_NAME, COMPLETE_STATE, PENDING_STATE, REGISTER_PAGE,
} from '../data/constants'; } from '../data/constants';
import { import {
getAllPossibleQueryParams, getTpaHint, getTpaProvider, isHostAvailableInQueryParams, setCookie, getAllPossibleQueryParams, getTpaHint, getTpaProvider, isHostAvailableInQueryParams, removeCookie, setCookie,
} from '../data/utils'; } from '../data/utils';
import { trackRegistrationPageViewed, trackRegistrationSuccess } from '../tracking/trackers/register';
/** /**
* Main Registration Page component * Main Registration Page component
@@ -68,6 +71,7 @@ const RegistrationPage = (props) => {
} = props; } = props;
const backedUpFormData = useSelector(state => state.register.registrationFormData); const backedUpFormData = useSelector(state => state.register.registrationFormData);
const initExpVariation = useSelector(state => state.register.autoGeneratedUsernameExperimentVariation);
const registrationError = useSelector(state => state.register.registrationError); const registrationError = useSelector(state => state.register.registrationError);
const registrationErrorCode = registrationError?.errorCode; const registrationErrorCode = registrationError?.errorCode;
const registrationResult = useSelector(state => state.register.registrationResult); const registrationResult = useSelector(state => state.register.registrationResult);
@@ -103,6 +107,12 @@ const RegistrationPage = (props) => {
? formatMessage(messages['create.account.cta.button'], { label: cta }) ? formatMessage(messages['create.account.cta.button'], { label: cta })
: formatMessage(messages['create.account.for.free.button']); : formatMessage(messages['create.account.for.free.button']);
const autoGeneratedUsernameExpVariation = useAutoGeneratedUsernameExperimentVariation(
initExpVariation, registrationEmbedded, tpaHint, currentProvider, thirdPartyAuthApiStatus,
);
const hideUsernameField = flags.autoGeneratedUsernameEnabled
|| autoGeneratedUsernameExpVariation === AUTO_GENERATED_USERNAME_REGISTRATION_EXP_VARIATION;
/** /**
* Set the userPipelineDetails data in formFields for only first time * Set the userPipelineDetails data in formFields for only first time
*/ */
@@ -128,7 +138,7 @@ const RegistrationPage = (props) => {
useEffect(() => { useEffect(() => {
if (!formStartTime) { if (!formStartTime) {
sendPageEvent('login_and_registration', 'register'); trackRegistrationPageViewed();
const payload = { ...queryParams, is_register_page: true }; const payload = { ...queryParams, is_register_page: true };
if (tpaHint) { if (tpaHint) {
payload.tpa_hint = tpaHint; payload.tpa_hint = tpaHint;
@@ -149,8 +159,10 @@ const RegistrationPage = (props) => {
formFields: { ...formFields }, formFields: { ...formFields },
errors: { ...errors }, errors: { ...errors },
})); }));
dispatch(setAutoGeneratedUsernameExperimentData(autoGeneratedUsernameExpVariation));
} }
}, [shouldBackupState, configurableFormFields, formFields, errors, dispatch, backedUpFormData]); }, [shouldBackupState, configurableFormFields, // eslint-disable-line react-hooks/exhaustive-deps
formFields, errors, dispatch, backedUpFormData]);
useEffect(() => { useEffect(() => {
if (backendValidations) { if (backendValidations) {
@@ -171,10 +183,15 @@ const RegistrationPage = (props) => {
useEffect(() => { useEffect(() => {
if (registrationResult.success) { if (registrationResult.success) {
// This event is used by GTM // This event is used by GTM
sendTrackEvent('edx.bi.user.account.registered.client', {}); trackRegistrationSuccess();
// This is used by the "User Retention Rate Event" on GTM // This is used by the "User Retention Rate Event" on GTM
setCookie(getConfig().USER_RETENTION_COOKIE_NAME, true); setCookie(getConfig().USER_RETENTION_COOKIE_NAME, true);
// Remove marketingEmailsOptIn cookie that was set on SSO registration flow
removeCookie('marketingEmailsOptIn');
// Remove this cookie that was set to capture marketingEmailsOptIn for the onboarding component
removeCookie('ssoPipelineRedirectionDone');
} }
}, [registrationResult]); }, [registrationResult]);
@@ -210,13 +227,13 @@ const RegistrationPage = (props) => {
const registerUser = () => { const registerUser = () => {
const totalRegistrationTime = (Date.now() - formStartTime) / 1000; const totalRegistrationTime = (Date.now() - formStartTime) / 1000;
let payload = { ...formFields }; let payload = { ...formFields, app_name: APP_NAME };
if (currentProvider) { if (currentProvider) {
delete payload.password; delete payload.password;
payload.social_auth_provider = currentProvider; payload.social_auth_provider = currentProvider;
} }
if (flags.autoGeneratedUsernameEnabled) { if (hideUsernameField) {
delete payload.username; delete payload.username;
} }
@@ -286,106 +303,109 @@ const RegistrationPage = (props) => {
getConfig().ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN && !!Object.keys(optionalFields.fields).length getConfig().ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN && !!Object.keys(optionalFields.fields).length
} }
/> />
{autoSubmitRegForm && !errorCode.type ? ( {(autoSubmitRegForm && !errorCode.type)
<div className="mw-xs mt-5 text-center"> || (!autoGeneratedUsernameExpVariation && !(
<Spinner animation="border" variant="primary" id="tpa-spinner" /> autoGeneratedUsernameExpVariation === NOT_INITIALIZED
</div> || registrationEmbedded || !!tpaHint || !!currentProvider))
) : ( ? (
<div <div className="mw-xs mt-5 text-center">
className={classNames( <Spinner animation="border" variant="primary" id="tpa-spinner" />
'mw-xs mt-3', </div>
{ 'w-100 m-auto pt-4 main-content': registrationEmbedded }, ) : (
)} <div
> className={classNames(
<ThirdPartyAuthAlert 'mw-xs mt-3',
currentProvider={currentProvider} { 'w-100 m-auto pt-4 main-content': registrationEmbedded },
platformName={platformName} )}
referrer={REGISTER_PAGE} >
/> <ThirdPartyAuthAlert
<RegistrationFailure currentProvider={currentProvider}
errorCode={errorCode.type} platformName={platformName}
failureCount={errorCode.count} referrer={REGISTER_PAGE}
context={{ provider: currentProvider, errorMessage: thirdPartyAuthErrorMessage }}
/>
<Form id="registration-form" name="registration-form">
<NameField
name="name"
value={formFields.name}
shouldFetchUsernameSuggestions={!formFields.username.trim()}
handleChange={handleOnChange}
handleErrorChange={handleErrorChange}
errorMessage={errors.name}
helpText={[formatMessage(messages['help.text.name'])]}
floatingLabel={formatMessage(messages['registration.fullname.label'])}
/> />
<EmailField <RegistrationFailure
name="email" errorCode={errorCode.type}
value={formFields.email} failureCount={errorCode.count}
confirmEmailValue={configurableFormFields?.confirm_email} context={{ provider: currentProvider, errorMessage: thirdPartyAuthErrorMessage }}
handleErrorChange={handleErrorChange}
handleChange={handleOnChange}
errorMessage={errors.email}
helpText={[formatMessage(messages['help.text.email'])]}
floatingLabel={formatMessage(messages['registration.email.label'])}
/> />
{!flags.autoGeneratedUsernameEnabled && ( <Form id="registration-form" name="registration-form">
<UsernameField <NameField
name="username" name="name"
spellCheck="false" value={formFields.name}
value={formFields.username} shouldFetchUsernameSuggestions={!formFields.username.trim()}
handleChange={handleOnChange} handleChange={handleOnChange}
handleErrorChange={handleErrorChange} handleErrorChange={handleErrorChange}
errorMessage={errors.username} errorMessage={errors.name}
helpText={[formatMessage(messages['help.text.username.1']), formatMessage(messages['help.text.username.2'])]} helpText={[formatMessage(messages['help.text.name'])]}
floatingLabel={formatMessage(messages['registration.username.label'])} floatingLabel={formatMessage(messages['registration.fullname.label'])}
/> />
)} <EmailField
{!currentProvider && ( name="email"
<PasswordField value={formFields.email}
name="password" confirmEmailValue={configurableFormFields?.confirm_email}
value={formFields.password}
handleChange={handleOnChange}
handleErrorChange={handleErrorChange} handleErrorChange={handleErrorChange}
errorMessage={errors.password} handleChange={handleOnChange}
floatingLabel={formatMessage(messages['registration.password.label'])} errorMessage={errors.email}
helpText={[formatMessage(messages['help.text.email'])]}
floatingLabel={formatMessage(messages['registration.email.label'])}
/> />
)} {!hideUsernameField && (
<ConfigurableRegistrationForm <UsernameField
email={formFields.email} name="username"
fieldErrors={errors} spellCheck="false"
formFields={configurableFormFields} value={formFields.username}
setFieldErrors={registrationEmbedded ? setTemporaryErrors : setErrors} handleChange={handleOnChange}
setFormFields={setConfigurableFormFields} handleErrorChange={handleErrorChange}
autoSubmitRegisterForm={autoSubmitRegForm} errorMessage={errors.username}
fieldDescriptions={fieldDescriptions} helpText={[formatMessage(messages['help.text.username.1']), formatMessage(messages['help.text.username.2'])]}
/> floatingLabel={formatMessage(messages['registration.username.label'])}
<StatefulButton />
id="register-user" )}
name="register-user" {!currentProvider && (
type="submit" <PasswordField
variant="brand" name="password"
className="register-button mt-4 mb-4" value={formFields.password}
state={submitState} handleChange={handleOnChange}
labels={{ handleErrorChange={handleErrorChange}
default: buttonLabel, errorMessage={errors.password}
pending: '', floatingLabel={formatMessage(messages['registration.password.label'])}
}} />
onClick={handleSubmit} )}
onMouseDown={(e) => e.preventDefault()} <ConfigurableRegistrationForm
/> email={formFields.email}
{!registrationEmbedded && ( fieldErrors={errors}
<ThirdPartyAuth formFields={configurableFormFields}
currentProvider={currentProvider} setFieldErrors={registrationEmbedded ? setTemporaryErrors : setErrors}
providers={providers} setFormFields={setConfigurableFormFields}
secondaryProviders={secondaryProviders} autoSubmitRegisterForm={autoSubmitRegForm}
handleInstitutionLogin={handleInstitutionLogin} fieldDescriptions={fieldDescriptions}
thirdPartyAuthApiStatus={thirdPartyAuthApiStatus}
/> />
)} <StatefulButton
</Form> id="register-user"
</div> name="register-user"
)} type="submit"
variant="brand"
className="register-button mt-4 mb-4"
state={submitState}
labels={{
default: buttonLabel,
pending: '',
}}
onClick={handleSubmit}
onMouseDown={(e) => e.preventDefault()}
/>
{!registrationEmbedded && (
<ThirdPartyAuth
currentProvider={currentProvider}
providers={providers}
secondaryProviders={secondaryProviders}
handleInstitutionLogin={handleInstitutionLogin}
thirdPartyAuthApiStatus={thirdPartyAuthApiStatus}
/>
)}
</Form>
</div>
)}
</> </>
); );
}; };

View File

@@ -1,9 +1,10 @@
import React from 'react';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { getConfig, mergeConfig } from '@edx/frontend-platform'; import { getConfig, mergeConfig } from '@edx/frontend-platform';
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics'; import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
import { import {
configure, getLocale, IntlProvider, configure, getLocale, injectIntl, IntlProvider,
} from '@edx/frontend-platform/i18n'; } from '@edx/frontend-platform/i18n';
import { fireEvent, render } from '@testing-library/react'; import { fireEvent, render } from '@testing-library/react';
import { mockNavigate, BrowserRouter as Router } from 'react-router-dom'; import { mockNavigate, BrowserRouter as Router } from 'react-router-dom';
@@ -16,9 +17,12 @@ import {
setUserPipelineDataLoaded, setUserPipelineDataLoaded,
} from './data/actions'; } from './data/actions';
import { INTERNAL_SERVER_ERROR } from './data/constants'; import { INTERNAL_SERVER_ERROR } from './data/constants';
import { NOT_INITIALIZED } from './data/optimizelyExperiment/helper';
import useAutoGeneratedUsernameExperimentVariation
from './data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation';
import RegistrationPage from './RegistrationPage'; import RegistrationPage from './RegistrationPage';
import { import {
AUTHN_PROGRESSIVE_PROFILING, COMPLETE_STATE, PENDING_STATE, REGISTER_PAGE, APP_NAME, AUTHN_PROGRESSIVE_PROFILING, COMPLETE_STATE, PENDING_STATE, REGISTER_PAGE,
} from '../data/constants'; } from '../data/constants';
jest.mock('@edx/frontend-platform/analytics', () => ({ jest.mock('@edx/frontend-platform/analytics', () => ({
@@ -29,7 +33,9 @@ jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'), ...jest.requireActual('@edx/frontend-platform/i18n'),
getLocale: jest.fn(), getLocale: jest.fn(),
})); }));
jest.mock('./data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation', () => jest.fn());
const IntlRegistrationPage = injectIntl(RegistrationPage);
const mockStore = configureStore(); const mockStore = configureStore();
jest.mock('react-router-dom', () => { jest.mock('react-router-dom', () => {
@@ -126,6 +132,7 @@ describe('RegistrationPage', () => {
institutionLogin: false, institutionLogin: false,
}; };
window.location = { search: '' }; window.location = { search: '' };
useAutoGeneratedUsernameExperimentVariation.mockReturnValue(NOT_INITIALIZED);
}); });
afterEach(() => { afterEach(() => {
@@ -183,10 +190,11 @@ describe('RegistrationPage', () => {
honor_code: true, honor_code: true,
total_registration_time: 0, total_registration_time: 0,
next: '/course/demo-course-url', next: '/course/demo-course-url',
app_name: APP_NAME,
}; };
store.dispatch = jest.fn(store.dispatch); store.dispatch = jest.fn(store.dispatch);
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />))); const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
populateRequiredFields(getByLabelText, payload); populateRequiredFields(getByLabelText, payload);
const button = container.querySelector('button.btn-brand'); const button = container.querySelector('button.btn-brand');
fireEvent.click(button); fireEvent.click(button);
@@ -205,6 +213,7 @@ describe('RegistrationPage', () => {
honor_code: true, honor_code: true,
social_auth_provider: 'Apple', social_auth_provider: 'Apple',
total_registration_time: 0, total_registration_time: 0,
app_name: APP_NAME,
}; };
store = mockStore({ store = mockStore({
@@ -218,7 +227,7 @@ describe('RegistrationPage', () => {
}, },
}); });
store.dispatch = jest.fn(store.dispatch); store.dispatch = jest.fn(store.dispatch);
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />))); const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
populateRequiredFields(getByLabelText, formPayload, true); populateRequiredFields(getByLabelText, formPayload, true);
const button = container.querySelector('button.btn-brand'); const button = container.querySelector('button.btn-brand');
@@ -241,7 +250,7 @@ describe('RegistrationPage', () => {
}; };
store.dispatch = jest.fn(store.dispatch); store.dispatch = jest.fn(store.dispatch);
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />))); const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
populateRequiredFields(getByLabelText, formPayload, true); populateRequiredFields(getByLabelText, formPayload, true);
const button = container.querySelector('button.btn-brand'); const button = container.querySelector('button.btn-brand');
@@ -266,7 +275,7 @@ describe('RegistrationPage', () => {
}; };
store.dispatch = jest.fn(store.dispatch); store.dispatch = jest.fn(store.dispatch);
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />))); const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
populateRequiredFields(getByLabelText, formPayload, true); populateRequiredFields(getByLabelText, formPayload, true);
const button = container.querySelector('button.btn-brand'); const button = container.querySelector('button.btn-brand');
fireEvent.click(button); fireEvent.click(button);
@@ -290,10 +299,11 @@ describe('RegistrationPage', () => {
honor_code: true, honor_code: true,
total_registration_time: 0, total_registration_time: 0,
marketing_emails_opt_in: true, marketing_emails_opt_in: true,
app_name: APP_NAME,
}; };
store.dispatch = jest.fn(store.dispatch); store.dispatch = jest.fn(store.dispatch);
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />))); const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
populateRequiredFields(getByLabelText, payload); populateRequiredFields(getByLabelText, payload);
const button = container.querySelector('button.btn-brand'); const button = container.querySelector('button.btn-brand');
fireEvent.click(button); fireEvent.click(button);
@@ -316,10 +326,11 @@ describe('RegistrationPage', () => {
country: 'Pakistan', country: 'Pakistan',
honor_code: true, honor_code: true,
total_registration_time: 0, total_registration_time: 0,
app_name: APP_NAME,
}; };
store.dispatch = jest.fn(store.dispatch); store.dispatch = jest.fn(store.dispatch);
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />))); const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
populateRequiredFields(getByLabelText, payload, false, true); populateRequiredFields(getByLabelText, payload, false, true);
const button = container.querySelector('button.btn-brand'); const button = container.querySelector('button.btn-brand');
fireEvent.click(button); fireEvent.click(button);
@@ -334,7 +345,7 @@ describe('RegistrationPage', () => {
ENABLE_AUTO_GENERATED_USERNAME: true, ENABLE_AUTO_GENERATED_USERNAME: true,
}); });
const { queryByLabelText } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />))); const { queryByLabelText } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
expect(queryByLabelText('Username')).toBeNull(); expect(queryByLabelText('Username')).toBeNull();
mergeConfig({ mergeConfig({
@@ -345,7 +356,7 @@ describe('RegistrationPage', () => {
it('should not dispatch registerNewUser on empty form Submission', () => { it('should not dispatch registerNewUser on empty form Submission', () => {
store.dispatch = jest.fn(store.dispatch); store.dispatch = jest.fn(store.dispatch);
const { container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />))); const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
const button = container.querySelector('button.btn-brand'); const button = container.querySelector('button.btn-brand');
fireEvent.click(button); fireEvent.click(button);
@@ -356,7 +367,7 @@ describe('RegistrationPage', () => {
// ******** test registration form validations ******** // ******** test registration form validations ********
it('should show error messages for required fields on empty form submission', () => { it('should show error messages for required fields on empty form submission', () => {
const { container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />))); const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
const button = container.querySelector('button.btn-brand'); const button = container.querySelector('button.btn-brand');
fireEvent.click(button); fireEvent.click(button);
@@ -384,7 +395,7 @@ describe('RegistrationPage', () => {
}, },
}, },
}); });
const { container } = render(routerWrapper(reduxWrapper(<IntlProvider locale="en"><RegistrationPage {...props} /></IntlProvider>))); const { container } = render(routerWrapper(reduxWrapper(<IntlProvider locale="en"><IntlRegistrationPage {...props} /></IntlProvider>)));
const usernameFeedback = container.querySelector('div[feedback-for="username"]'); const usernameFeedback = container.querySelector('div[feedback-for="username"]');
const emailFeedback = container.querySelector('div[feedback-for="email"]'); const emailFeedback = container.querySelector('div[feedback-for="email"]');
@@ -393,7 +404,7 @@ describe('RegistrationPage', () => {
}); });
it('should clear error on focus', () => { it('should clear error on focus', () => {
const { container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />))); const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
const submitButton = container.querySelector('button.btn-brand'); const submitButton = container.querySelector('button.btn-brand');
fireEvent.click(submitButton); fireEvent.click(submitButton);
@@ -422,7 +433,7 @@ describe('RegistrationPage', () => {
store.dispatch = jest.fn(store.dispatch); store.dispatch = jest.fn(store.dispatch);
const { container } = render(routerWrapper(reduxWrapper( const { container } = render(routerWrapper(reduxWrapper(
<RegistrationPage {...props} />, <IntlRegistrationPage {...props} />,
))); )));
const emailInput = container.querySelector('input#email'); const emailInput = container.querySelector('input#email');
@@ -433,7 +444,7 @@ describe('RegistrationPage', () => {
// ******** test form buttons and fields ******** // ******** test form buttons and fields ********
it('should match default button state', () => { it('should match default button state', () => {
const { container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />))); const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
const button = container.querySelector('button[type="submit"] span'); const button = container.querySelector('button[type="submit"] span');
expect(button.textContent).toEqual('Create an account for free'); expect(button.textContent).toEqual('Create an account for free');
}); });
@@ -447,7 +458,7 @@ describe('RegistrationPage', () => {
}, },
}); });
const { container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />))); const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
const button = container.querySelector('button[type="submit"] span.sr-only'); const button = container.querySelector('button[type="submit"] span.sr-only');
expect(button.textContent).toEqual('pending'); expect(button.textContent).toEqual('pending');
@@ -458,7 +469,7 @@ describe('RegistrationPage', () => {
MARKETING_EMAILS_OPT_IN: 'true', MARKETING_EMAILS_OPT_IN: 'true',
}); });
const { container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />))); const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
const checkboxDivs = container.querySelectorAll('div.form-field--checkbox'); const checkboxDivs = container.querySelectorAll('div.form-field--checkbox');
expect(checkboxDivs.length).toEqual(1); expect(checkboxDivs.length).toEqual(1);
@@ -471,7 +482,7 @@ describe('RegistrationPage', () => {
const buttonLabel = 'Register'; const buttonLabel = 'Register';
delete window.location; delete window.location;
window.location = { href: getConfig().BASE_URL, search: `?cta=${buttonLabel}` }; window.location = { href: getConfig().BASE_URL, search: `?cta=${buttonLabel}` };
const { container } = render(reduxWrapper(<RegistrationPage {...props} />)); const { container } = render(reduxWrapper(<IntlRegistrationPage {...props} />));
const button = container.querySelector('button[type="submit"] span'); const button = container.querySelector('button[type="submit"] span');
const buttonText = button.textContent; const buttonText = button.textContent;
@@ -490,7 +501,7 @@ describe('RegistrationPage', () => {
}, },
}); });
render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />))); render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
expect(document.cookie).toMatch(`${getConfig().USER_RETENTION_COOKIE_NAME}=true`); expect(document.cookie).toMatch(`${getConfig().USER_RETENTION_COOKIE_NAME}=true`);
}); });
@@ -508,7 +519,7 @@ describe('RegistrationPage', () => {
}); });
delete window.location; delete window.location;
window.location = { href: getConfig().BASE_URL }; window.location = { href: getConfig().BASE_URL };
render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />))); render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
expect(window.location.href).toBe(dashboardURL); expect(window.location.href).toBe(dashboardURL);
}); });
@@ -535,7 +546,7 @@ describe('RegistrationPage', () => {
}); });
delete window.location; delete window.location;
window.location = { href: getConfig().BASE_URL }; window.location = { href: getConfig().BASE_URL };
render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />))); render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
expect(window.location.href).toBe(dashboardUrl); expect(window.location.href).toBe(dashboardUrl);
}); });
@@ -566,7 +577,7 @@ describe('RegistrationPage', () => {
render(reduxWrapper( render(reduxWrapper(
<Router> <Router>
<RegistrationPage {...props} /> <IntlRegistrationPage {...props} />
</Router>, </Router>,
)); ));
expect(mockNavigate).toHaveBeenCalledWith(AUTHN_PROGRESSIVE_PROFILING); expect(mockNavigate).toHaveBeenCalledWith(AUTHN_PROGRESSIVE_PROFILING);
@@ -584,13 +595,13 @@ describe('RegistrationPage', () => {
}); });
store.dispatch = jest.fn(store.dispatch); store.dispatch = jest.fn(store.dispatch);
render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />))); render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
expect(store.dispatch).toHaveBeenCalledWith(backupRegistrationFormBegin({ ...registrationFormData })); expect(store.dispatch).toHaveBeenCalledWith(backupRegistrationFormBegin({ ...registrationFormData }));
}); });
it('should send page event when register page is rendered', () => { it('should send page event when register page is rendered', () => {
render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />))); render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
expect(sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'register'); expect(sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'register', { app_name: APP_NAME });
}); });
it('should send track event when user has successfully registered', () => { it('should send track event when user has successfully registered', () => {
@@ -607,8 +618,8 @@ describe('RegistrationPage', () => {
delete window.location; delete window.location;
window.location = { href: getConfig().BASE_URL }; window.location = { href: getConfig().BASE_URL };
render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />))); render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.user.account.registered.client', {}); expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.user.account.registered.client', { app_name: APP_NAME });
}); });
it('should populate form with pipeline user details', () => { it('should populate form with pipeline user details', () => {
@@ -633,7 +644,7 @@ describe('RegistrationPage', () => {
store.dispatch = jest.fn(store.dispatch); store.dispatch = jest.fn(store.dispatch);
const { container } = render(reduxWrapper( const { container } = render(reduxWrapper(
<Router> <Router>
<RegistrationPage {...props} /> <IntlRegistrationPage {...props} />
</Router>, </Router>,
)); ));
@@ -656,7 +667,7 @@ describe('RegistrationPage', () => {
}, },
}); });
const { container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />))); const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
const validationErrors = container.querySelector('div#validation-errors'); const validationErrors = container.querySelector('div#validation-errors');
expect(validationErrors.textContent).toContain( expect(validationErrors.textContent).toContain(
'An error has occurred. Try refreshing the page, or check your internet connection.', 'An error has occurred. Try refreshing the page, or check your internet connection.',
@@ -683,7 +694,7 @@ describe('RegistrationPage', () => {
}, },
}); });
const { container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />))); const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
const fullNameInput = container.querySelector('input#name'); const fullNameInput = container.querySelector('input#name');
const usernameInput = container.querySelector('input#username'); const usernameInput = container.querySelector('input#username');
@@ -729,14 +740,14 @@ describe('RegistrationPage', () => {
}, },
}, },
}); });
render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />))); render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
expect(window.parent.postMessage).toHaveBeenCalledTimes(2); expect(window.parent.postMessage).toHaveBeenCalledTimes(2);
}); });
it('should not display validations error on blur event when embedded variant is rendered', () => { it('should not display validations error on blur event when embedded variant is rendered', () => {
delete window.location; delete window.location;
window.location = { href: getConfig().BASE_URL.concat(REGISTER_PAGE), search: '?host=http://localhost/host-website' }; window.location = { href: getConfig().BASE_URL.concat(REGISTER_PAGE), search: '?host=http://localhost/host-website' };
const { container } = render(reduxWrapper(<RegistrationPage {...props} />)); const { container } = render(reduxWrapper(<IntlRegistrationPage {...props} />));
const usernameInput = container.querySelector('input#username'); const usernameInput = container.querySelector('input#username');
fireEvent.blur(usernameInput, { target: { value: '', name: 'username' } }); fireEvent.blur(usernameInput, { target: { value: '', name: 'username' } });
@@ -764,7 +775,7 @@ describe('RegistrationPage', () => {
}, },
}); });
const { container } = render(routerWrapper(reduxWrapper( const { container } = render(routerWrapper(reduxWrapper(
<RegistrationPage {...props} />), <IntlRegistrationPage {...props} />),
)); ));
const usernameFeedback = container.querySelector('div[feedback-for="username"]'); const usernameFeedback = container.querySelector('div[feedback-for="username"]');
@@ -781,7 +792,7 @@ describe('RegistrationPage', () => {
search: '?host=http://localhost/host-website', search: '?host=http://localhost/host-website',
}; };
const { container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />))); const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
const submitButton = container.querySelector('button.btn-brand'); const submitButton = container.querySelector('button.btn-brand');
fireEvent.click(submitButton); fireEvent.click(submitButton);
@@ -822,7 +833,7 @@ describe('RegistrationPage', () => {
}); });
store.dispatch = jest.fn(store.dispatch); store.dispatch = jest.fn(store.dispatch);
const { container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />))); const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
const spinnerElement = container.querySelector('#tpa-spinner'); const spinnerElement = container.querySelector('#tpa-spinner');
const registrationFormElement = container.querySelector('#registration-form'); const registrationFormElement = container.querySelector('#registration-form');
@@ -873,7 +884,7 @@ describe('RegistrationPage', () => {
}); });
store.dispatch = jest.fn(store.dispatch); store.dispatch = jest.fn(store.dispatch);
render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />))); render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({
name: 'John Doe', name: 'John Doe',
username: 'john_doe', username: 'john_doe',
@@ -881,6 +892,7 @@ describe('RegistrationPage', () => {
country: 'PK', country: 'PK',
social_auth_provider: 'Apple', social_auth_provider: 'Apple',
total_registration_time: 0, total_registration_time: 0,
app_name: APP_NAME,
})); }));
}); });
}); });

View File

@@ -1,10 +1,12 @@
import React, { useEffect, useMemo } from 'react'; import React, { useEffect, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { getConfig } from '@edx/frontend-platform'; import { getConfig } from '@edx/frontend-platform';
import { getCountryList, getLocale, useIntl } from '@edx/frontend-platform/i18n'; import { getCountryList, getLocale, useIntl } from '@edx/frontend-platform/i18n';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { FormFieldRenderer } from '../../field-renderer'; import { FormFieldRenderer } from '../../field-renderer';
import { backupRegistrationFormBegin } from '../data/actions';
import { FIELDS } from '../data/constants'; import { FIELDS } from '../data/constants';
import messages from '../messages'; import messages from '../messages';
import { CountryField, HonorCode, TermsOfService } from '../RegistrationFields'; import { CountryField, HonorCode, TermsOfService } from '../RegistrationFields';
@@ -32,12 +34,16 @@ const ConfigurableRegistrationForm = (props) => {
setFormFields, setFormFields,
autoSubmitRegistrationForm, autoSubmitRegistrationForm,
} = props; } = props;
const dispatch = useDispatch();
/** The reason for adding the entry 'United States' is that Chrome browser aut-fill the form with the 'Unites /** 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 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. 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' }]), []);
const countryList = useMemo(() => (
getCountryList(getLocale()).concat([{ code: 'US', name: 'United States' }]).filter(country => country.code !== 'RU')
), []);
let showTermsOfServiceAndHonorCode = false; let showTermsOfServiceAndHonorCode = false;
let showCountryField = false; let showCountryField = false;
@@ -50,6 +56,8 @@ const ConfigurableRegistrationForm = (props) => {
showMarketingEmailOptInCheckbox: getConfig().MARKETING_EMAILS_OPT_IN, showMarketingEmailOptInCheckbox: getConfig().MARKETING_EMAILS_OPT_IN,
}; };
const backedUpFormData = useSelector(state => state.register.registrationFormData);
/** /**
* If auto submitting register form, we will check tos and honor code fields if they exist for feature parity. * If auto submitting register form, we will check tos and honor code fields if they exist for feature parity.
*/ */
@@ -90,6 +98,16 @@ const ConfigurableRegistrationForm = (props) => {
setFieldErrors(prevErrors => ({ ...prevErrors, [name]: '' })); setFieldErrors(prevErrors => ({ ...prevErrors, [name]: '' }));
} }
} }
// setting marketingEmailsOptIn state for SSO authentication flow for register API call
if (name === 'marketingEmailsOptIn') {
dispatch(backupRegistrationFormBegin({
...backedUpFormData,
configurableFormFields: {
...backedUpFormData.configurableFormFields,
[name]: value,
},
}));
}
setFormFields(prevState => ({ ...prevState, [name]: value })); setFormFields(prevState => ({ ...prevState, [name]: value }));
}; };

View File

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

View File

@@ -1,15 +1,20 @@
import React from 'react';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { mergeConfig } from '@edx/frontend-platform'; import { mergeConfig } from '@edx/frontend-platform';
import { import {
getLocale, IntlProvider, getLocale, injectIntl, IntlProvider,
} from '@edx/frontend-platform/i18n'; } from '@edx/frontend-platform/i18n';
import { fireEvent, render } from '@testing-library/react'; import { fireEvent, render } from '@testing-library/react';
import { BrowserRouter as Router } from 'react-router-dom'; import { BrowserRouter as Router } from 'react-router-dom';
import configureStore from 'redux-mock-store'; import configureStore from 'redux-mock-store';
import { APP_NAME } from '../../../data/constants';
import { registerNewUser } from '../../data/actions'; import { registerNewUser } from '../../data/actions';
import { FIELDS } from '../../data/constants'; import { FIELDS } from '../../data/constants';
import { NOT_INITIALIZED } from '../../data/optimizelyExperiment/helper';
import useAutoGeneratedUsernameExperimentVariation
from '../../data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation';
import RegistrationPage from '../../RegistrationPage'; import RegistrationPage from '../../RegistrationPage';
import ConfigurableRegistrationForm from '../ConfigurableRegistrationForm'; import ConfigurableRegistrationForm from '../ConfigurableRegistrationForm';
@@ -21,7 +26,10 @@ jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'), ...jest.requireActual('@edx/frontend-platform/i18n'),
getLocale: jest.fn(), getLocale: jest.fn(),
})); }));
jest.mock('../../data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation', () => jest.fn());
const IntlConfigurableRegistrationForm = injectIntl(ConfigurableRegistrationForm);
const IntlRegistrationPage = injectIntl(RegistrationPage);
const mockStore = configureStore(); const mockStore = configureStore();
jest.mock('react-router-dom', () => { jest.mock('react-router-dom', () => {
@@ -118,6 +126,7 @@ describe('ConfigurableRegistrationForm', () => {
}; };
window.location = { search: '' }; window.location = { search: '' };
getLocale.mockImplementationOnce(() => ('en-us')); getLocale.mockImplementationOnce(() => ('en-us'));
useAutoGeneratedUsernameExperimentVariation.mockReturnValue(NOT_INITIALIZED);
}); });
afterEach(() => { afterEach(() => {
@@ -155,7 +164,7 @@ describe('ConfigurableRegistrationForm', () => {
}; };
render(routerWrapper(reduxWrapper( render(routerWrapper(reduxWrapper(
<ConfigurableRegistrationForm {...props} />, <IntlConfigurableRegistrationForm {...props} />,
))); )));
expect(document.querySelector('#profession')).toBeTruthy(); expect(document.querySelector('#profession')).toBeTruthy();
@@ -185,7 +194,7 @@ describe('ConfigurableRegistrationForm', () => {
}; };
render(routerWrapper(reduxWrapper( render(routerWrapper(reduxWrapper(
<ConfigurableRegistrationForm {...props} />, <IntlConfigurableRegistrationForm {...props} />,
))); )));
expect(props.setFormFields).toHaveBeenCalledTimes(2); expect(props.setFormFields).toHaveBeenCalledTimes(2);
@@ -212,7 +221,7 @@ describe('ConfigurableRegistrationForm', () => {
}, },
}, },
}); });
render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />))); render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
expect(document.querySelector('#profession')).toBeTruthy(); expect(document.querySelector('#profession')).toBeTruthy();
expect(document.querySelector('#tos')).toBeTruthy(); expect(document.querySelector('#tos')).toBeTruthy();
}); });
@@ -246,7 +255,7 @@ describe('ConfigurableRegistrationForm', () => {
}; };
store.dispatch = jest.fn(store.dispatch); store.dispatch = jest.fn(store.dispatch);
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />))); const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
populateRequiredFields(getByLabelText, payload); populateRequiredFields(getByLabelText, payload);
@@ -257,7 +266,7 @@ describe('ConfigurableRegistrationForm', () => {
fireEvent.click(submitButton); fireEvent.click(submitButton);
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK' })); expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK', app_name: APP_NAME }));
}); });
it('should show error messages for required fields on empty form submission', () => { it('should show error messages for required fields on empty form submission', () => {
@@ -281,7 +290,7 @@ describe('ConfigurableRegistrationForm', () => {
}, },
}); });
const { container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />))); const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
const submitButton = container.querySelector('button.btn-brand'); const submitButton = container.querySelector('button.btn-brand');
fireEvent.click(submitButton); fireEvent.click(submitButton);
@@ -307,7 +316,7 @@ describe('ConfigurableRegistrationForm', () => {
}, },
}, },
}); });
const { container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />))); const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
const countryInput = container.querySelector('input[name="country"]'); const countryInput = container.querySelector('input[name="country"]');
fireEvent.change(countryInput, { target: { value: 'Pak', name: 'country' } }); fireEvent.change(countryInput, { target: { value: 'Pak', name: 'country' } });
fireEvent.blur(countryInput, { target: { value: 'Pak', name: 'country' } }); fireEvent.blur(countryInput, { target: { value: 'Pak', name: 'country' } });
@@ -332,7 +341,7 @@ describe('ConfigurableRegistrationForm', () => {
}, },
}, },
}); });
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />))); const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
const emailInput = getByLabelText('Email'); const emailInput = getByLabelText('Email');
const confirmEmailInput = getByLabelText('Confirm Email'); const confirmEmailInput = getByLabelText('Confirm Email');
@@ -368,7 +377,7 @@ describe('ConfigurableRegistrationForm', () => {
}, },
}, },
}); });
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />))); const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
populateRequiredFields(getByLabelText, formPayload, true); populateRequiredFields(getByLabelText, formPayload, true);
fireEvent.change( fireEvent.change(
@@ -403,7 +412,7 @@ describe('ConfigurableRegistrationForm', () => {
}); });
const { getByLabelText, container } = render( const { getByLabelText, container } = render(
routerWrapper(reduxWrapper(<RegistrationPage {...props} />)), routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)),
); );
const professionInput = getByLabelText('Profession'); const professionInput = getByLabelText('Profession');

View File

@@ -1,8 +1,9 @@
import React from 'react';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { mergeConfig } from '@edx/frontend-platform'; import { mergeConfig } from '@edx/frontend-platform';
import { import {
configure, getLocale, IntlProvider, configure, getLocale, injectIntl, IntlProvider,
} from '@edx/frontend-platform/i18n'; } from '@edx/frontend-platform/i18n';
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { BrowserRouter as Router } from 'react-router-dom'; import { BrowserRouter as Router } from 'react-router-dom';
@@ -11,6 +12,9 @@ import configureStore from 'redux-mock-store';
import { import {
FORBIDDEN_REQUEST, INTERNAL_SERVER_ERROR, TPA_AUTHENTICATION_FAILURE, TPA_SESSION_EXPIRED, FORBIDDEN_REQUEST, INTERNAL_SERVER_ERROR, TPA_AUTHENTICATION_FAILURE, TPA_SESSION_EXPIRED,
} from '../../data/constants'; } from '../../data/constants';
import { NOT_INITIALIZED } from '../../data/optimizelyExperiment/helper';
import useAutoGeneratedUsernameExperimentVariation
from '../../data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation';
import RegistrationPage from '../../RegistrationPage'; import RegistrationPage from '../../RegistrationPage';
import RegistrationFailureMessage from '../RegistrationFailure'; import RegistrationFailureMessage from '../RegistrationFailure';
@@ -22,7 +26,10 @@ jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'), ...jest.requireActual('@edx/frontend-platform/i18n'),
getLocale: jest.fn(), getLocale: jest.fn(),
})); }));
jest.mock('../../data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation', () => jest.fn());
const IntlRegistrationPage = injectIntl(RegistrationPage);
const IntlRegistrationFailure = injectIntl(RegistrationFailureMessage);
const mockStore = configureStore(); const mockStore = configureStore();
jest.mock('react-router-dom', () => { jest.mock('react-router-dom', () => {
@@ -118,6 +125,7 @@ describe('RegistrationFailure', () => {
institutionLogin: false, institutionLogin: false,
}; };
window.location = { search: '' }; window.location = { search: '' };
useAutoGeneratedUsernameExperimentVariation.mockReturnValue(NOT_INITIALIZED);
}); });
afterEach(() => { afterEach(() => {
@@ -134,7 +142,7 @@ describe('RegistrationFailure', () => {
failureCount: 0, failureCount: 0,
}; };
const { container } = render(reduxWrapper(<RegistrationFailureMessage {...props} />)); const { container } = render(reduxWrapper(<IntlRegistrationFailure {...props} />));
const alertHeading = container.querySelectorAll('div.alert-heading'); const alertHeading = container.querySelectorAll('div.alert-heading');
expect(alertHeading.length).toEqual(1); expect(alertHeading.length).toEqual(1);
@@ -150,7 +158,7 @@ describe('RegistrationFailure', () => {
failureCount: 0, failureCount: 0,
}; };
const { container } = render(reduxWrapper(<RegistrationFailureMessage {...props} />)); const { container } = render(reduxWrapper(<IntlRegistrationFailure {...props} />));
const alertHeading = container.querySelectorAll('div.alert-heading'); const alertHeading = container.querySelectorAll('div.alert-heading');
expect(alertHeading.length).toEqual(1); expect(alertHeading.length).toEqual(1);
@@ -169,7 +177,7 @@ describe('RegistrationFailure', () => {
failureCount: 0, failureCount: 0,
}; };
const { container } = render(reduxWrapper(<RegistrationFailureMessage {...props} />)); const { container } = render(reduxWrapper(<IntlRegistrationFailure {...props} />));
const alertHeading = container.querySelectorAll('div.alert-heading'); const alertHeading = container.querySelectorAll('div.alert-heading');
expect(alertHeading.length).toEqual(1); expect(alertHeading.length).toEqual(1);
@@ -188,7 +196,7 @@ describe('RegistrationFailure', () => {
failureCount: 0, failureCount: 0,
}; };
const { container } = render(reduxWrapper(<RegistrationFailureMessage {...props} />)); const { container } = render(reduxWrapper(<IntlRegistrationFailure {...props} />));
const alertHeading = container.querySelectorAll('div.alert-heading'); const alertHeading = container.querySelectorAll('div.alert-heading');
expect(alertHeading.length).toEqual(1); expect(alertHeading.length).toEqual(1);
@@ -208,7 +216,7 @@ describe('RegistrationFailure', () => {
}, },
}); });
render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />))); render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
const validationError = screen.queryByText('An error has occurred. Try refreshing the page, or check your internet connection.'); const validationError = screen.queryByText('An error has occurred. Try refreshing the page, or check your internet connection.');
expect(validationError).not.toBeNull(); expect(validationError).not.toBeNull();

View File

@@ -1,8 +1,9 @@
import React from 'react';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { getConfig, mergeConfig } from '@edx/frontend-platform'; import { getConfig, mergeConfig } from '@edx/frontend-platform';
import { import {
configure, getLocale, IntlProvider, configure, getLocale, injectIntl, IntlProvider,
} from '@edx/frontend-platform/i18n'; } from '@edx/frontend-platform/i18n';
import { fireEvent, render } from '@testing-library/react'; import { fireEvent, render } from '@testing-library/react';
import { BrowserRouter as Router } from 'react-router-dom'; import { BrowserRouter as Router } from 'react-router-dom';
@@ -11,6 +12,9 @@ import configureStore from 'redux-mock-store';
import { import {
COMPLETE_STATE, LOGIN_PAGE, PENDING_STATE, REGISTER_PAGE, COMPLETE_STATE, LOGIN_PAGE, PENDING_STATE, REGISTER_PAGE,
} from '../../../data/constants'; } from '../../../data/constants';
import { NOT_INITIALIZED } from '../../data/optimizelyExperiment/helper';
import useAutoGeneratedUsernameExperimentVariation
from '../../data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation';
import RegistrationPage from '../../RegistrationPage'; import RegistrationPage from '../../RegistrationPage';
jest.mock('@edx/frontend-platform/analytics', () => ({ jest.mock('@edx/frontend-platform/analytics', () => ({
@@ -21,7 +25,9 @@ jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'), ...jest.requireActual('@edx/frontend-platform/i18n'),
getLocale: jest.fn(), getLocale: jest.fn(),
})); }));
jest.mock('../../data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation', () => jest.fn());
const IntlRegistrationPage = injectIntl(RegistrationPage);
const mockStore = configureStore(); const mockStore = configureStore();
jest.mock('react-router-dom', () => { jest.mock('react-router-dom', () => {
@@ -118,6 +124,7 @@ describe('ThirdPartyAuth', () => {
institutionLogin: false, institutionLogin: false,
}; };
window.location = { search: '' }; window.location = { search: '' };
useAutoGeneratedUsernameExperimentVariation.mockReturnValue(NOT_INITIALIZED);
}); });
afterEach(() => { afterEach(() => {
@@ -155,7 +162,7 @@ describe('ThirdPartyAuth', () => {
}); });
const { queryByLabelText } = render( const { queryByLabelText } = render(
routerWrapper(reduxWrapper(<RegistrationPage {...props} />, { store })), routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />, { store })),
); );
const passwordField = queryByLabelText('Password'); const passwordField = queryByLabelText('Password');
@@ -180,7 +187,7 @@ describe('ThirdPartyAuth', () => {
window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: `?next=/dashboard&tpa_hint=${ssoProvider.id}` }; window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: `?next=/dashboard&tpa_hint=${ssoProvider.id}` };
const { container } = render( const { container } = render(
routerWrapper(reduxWrapper(<RegistrationPage {...props} />)), routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)),
); );
const tpaButton = container.querySelector(`button#${ssoProvider.id}`); const tpaButton = container.querySelector(`button#${ssoProvider.id}`);
@@ -205,7 +212,7 @@ describe('ThirdPartyAuth', () => {
search: `?next=/dashboard&tpa_hint=${ssoProvider.id}`, search: `?next=/dashboard&tpa_hint=${ssoProvider.id}`,
}; };
const { container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />))); const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
const skeletonElement = container.querySelector('.react-loading-skeleton'); const skeletonElement = container.querySelector('.react-loading-skeleton');
expect(skeletonElement).toBeTruthy(); expect(skeletonElement).toBeTruthy();
@@ -229,7 +236,7 @@ describe('ThirdPartyAuth', () => {
window.location = { href: getConfig().BASE_URL.concat(REGISTER_PAGE), search: `?next=/dashboard&tpa_hint=${ssoProvider.id}` }; window.location = { href: getConfig().BASE_URL.concat(REGISTER_PAGE), search: `?next=/dashboard&tpa_hint=${ssoProvider.id}` };
ssoProvider.iconImage = null; ssoProvider.iconImage = null;
const { container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />))); const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
const iconElement = container.querySelector(`button#${ssoProvider.id} div span.pgn__icon`); const iconElement = container.querySelector(`button#${ssoProvider.id} div span.pgn__icon`);
expect(iconElement).toBeTruthy(); expect(iconElement).toBeTruthy();
@@ -252,7 +259,7 @@ describe('ThirdPartyAuth', () => {
delete window.location; delete window.location;
window.location = { href: getConfig().BASE_URL.concat(REGISTER_PAGE), search: `?next=/dashboard&tpa_hint=${secondaryProviders.id}` }; window.location = { href: getConfig().BASE_URL.concat(REGISTER_PAGE), search: `?next=/dashboard&tpa_hint=${secondaryProviders.id}` };
render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />))); render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
expect(window.location.href).toEqual(getConfig().LMS_BASE_URL + secondaryProviders.registerUrl); expect(window.location.href).toEqual(getConfig().LMS_BASE_URL + secondaryProviders.registerUrl);
}); });
@@ -273,7 +280,7 @@ describe('ThirdPartyAuth', () => {
delete window.location; delete window.location;
window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: '?next=/dashboard&tpa_hint=invalid' }; window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: '?next=/dashboard&tpa_hint=invalid' };
const { container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />))); const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
const providerButton = container.querySelector(`button#${ssoProvider.id} span#provider-name`); const providerButton = container.querySelector(`button#${ssoProvider.id} span#provider-name`);
expect(providerButton.textContent).toEqual(expectedMessage); expect(providerButton.textContent).toEqual(expectedMessage);
@@ -292,7 +299,7 @@ describe('ThirdPartyAuth', () => {
}); });
const { container } = render( const { container } = render(
routerWrapper(reduxWrapper(<RegistrationPage {...props} />, { store })), routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />, { store })),
); );
const buttonsWithId = container.querySelectorAll(`button#${ssoProvider.id}`); const buttonsWithId = container.querySelectorAll(`button#${ssoProvider.id}`);
@@ -313,7 +320,7 @@ describe('ThirdPartyAuth', () => {
}); });
const { container } = render( const { container } = render(
routerWrapper(reduxWrapper(<RegistrationPage {...props} />)), routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)),
); );
const buttonsWithId = container.querySelectorAll(`button#${ssoProvider.id}`); const buttonsWithId = container.querySelectorAll(`button#${ssoProvider.id}`);
@@ -327,7 +334,7 @@ describe('ThirdPartyAuth', () => {
institutionLogin: true, institutionLogin: true,
}; };
const { getByText } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />))); const { getByText } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
const headingElement = getByText('Register with institution/campus credentials'); const headingElement = getByText('Register with institution/campus credentials');
expect(headingElement).toBeTruthy(); expect(headingElement).toBeTruthy();
}); });
@@ -352,7 +359,7 @@ describe('ThirdPartyAuth', () => {
window.location = { href: getConfig().BASE_URL }; window.location = { href: getConfig().BASE_URL };
const { container } = render( const { container } = render(
routerWrapper(reduxWrapper(<RegistrationPage {...props} />)), routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)),
); );
const ssoButton = container.querySelector('button#oa2-apple-id'); const ssoButton = container.querySelector('button#oa2-apple-id');
@@ -383,7 +390,7 @@ describe('ThirdPartyAuth', () => {
delete window.location; delete window.location;
window.location = { href: getConfig().BASE_URL }; window.location = { href: getConfig().BASE_URL };
render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />))); render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + authCompleteUrl); expect(window.location.href).toBe(getConfig().LMS_BASE_URL + authCompleteUrl);
}); });
@@ -404,7 +411,7 @@ describe('ThirdPartyAuth', () => {
const expectedMessage = `${'You\'ve successfully signed into Apple! We just need a little more information before ' const expectedMessage = `${'You\'ve successfully signed into Apple! We just need a little more information before '
+ 'you start learning with '}${ getConfig().SITE_NAME }.`; + 'you start learning with '}${ getConfig().SITE_NAME }.`;
const { container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />))); const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
const tpaAlert = container.querySelector('#tpa-alert p'); const tpaAlert = container.querySelector('#tpa-alert p');
expect(tpaAlert.textContent).toEqual(expectedMessage); expect(tpaAlert.textContent).toEqual(expectedMessage);
}); });
@@ -435,7 +442,7 @@ describe('ThirdPartyAuth', () => {
store.dispatch = jest.fn(store.dispatch); store.dispatch = jest.fn(store.dispatch);
const { container } = render( const { container } = render(
routerWrapper(reduxWrapper(<RegistrationPage {...props} />)), routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)),
); );
const alertHeading = container.querySelector('div.alert-heading'); const alertHeading = container.querySelector('div.alert-heading');

View File

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

View File

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

View File

@@ -0,0 +1,30 @@
/**
* This file contains data for auto generated username Optimizely experiment
*/
import { getConfig } from '@edx/frontend-platform';
export const NOT_INITIALIZED = 'experiment-not-initialized';
export const CONTROL = 'control-registration-page';
export const AUTO_GENERATED_USERNAME_REGISTRATION_EXP_VARIATION = 'auto-generated-username-register-page';
const AUTO_GENERATED_USERNAME_EXP_PAGE = 'targeting_for_auto_generated_username_page';
export function getAutoGeneratedUsernameExperimentVariation() {
try {
if (window.optimizely
&& window.optimizely.get('data').experiments[getConfig().AUTO_GENERATED_USERNAME_EXPERIMENT_ID]) {
const selectedVariant = window.optimizely.get('state').getVariationMap()[
getConfig().AUTO_GENERATED_USERNAME_EXPERIMENT_ID
];
return selectedVariant?.name;
}
} catch (e) { /* empty */ }
return '';
}
export function activateAutoGeneratedUsernameExperiment() {
window.optimizely = window.optimizely || [];
window.optimizely.push({
type: 'page',
pageName: AUTO_GENERATED_USERNAME_EXP_PAGE,
});
}

View File

@@ -0,0 +1,53 @@
import { useEffect, useState } from 'react';
import {
activateAutoGeneratedUsernameExperiment,
getAutoGeneratedUsernameExperimentVariation,
NOT_INITIALIZED,
} from './helper';
import { COMPLETE_STATE } from '../../../data/constants';
/**
* This hook returns activates multi step registration experiment and returns the experiment
* variation for the user.
*/
const useAutoGeneratedUsernameExperimentVariation = (
initExpVariation,
registrationEmbedded,
tpaHint,
currentProvider,
thirdPartyAuthApiStatus,
) => {
const [variation, setVariation] = useState(initExpVariation);
useEffect(() => {
if (initExpVariation || registrationEmbedded || !!tpaHint || !!currentProvider
|| thirdPartyAuthApiStatus !== COMPLETE_STATE) {
return variation;
}
const getVariation = () => {
const expVariation = getAutoGeneratedUsernameExperimentVariation();
if (expVariation) {
setVariation(expVariation);
} else {
// This is to handle the case when user dont get variation for some reason, the register page
// shows unlimited spinner.
setVariation(NOT_INITIALIZED);
}
};
activateAutoGeneratedUsernameExperiment();
const timer = setTimeout(getVariation, 300);
return () => {
clearTimeout(timer);
};
}, [ // eslint-disable-line react-hooks/exhaustive-deps
initExpVariation, currentProvider, registrationEmbedded, thirdPartyAuthApiStatus, tpaHint,
]);
return variation;
};
export default useAutoGeneratedUsernameExperimentVariation;

View File

@@ -3,6 +3,7 @@ import {
REGISTER_CLEAR_USERNAME_SUGGESTIONS, REGISTER_CLEAR_USERNAME_SUGGESTIONS,
REGISTER_FORM_VALIDATIONS, REGISTER_FORM_VALIDATIONS,
REGISTER_NEW_USER, REGISTER_NEW_USER,
REGISTER_SET_AUTO_GENERATED_USERNAME_REGISTRATION_EXP_DATA,
REGISTER_SET_COUNTRY_CODE, REGISTER_SET_COUNTRY_CODE,
REGISTER_SET_EMAIL_SUGGESTIONS, REGISTER_SET_EMAIL_SUGGESTIONS,
REGISTER_SET_USER_PIPELINE_DATA_LOADED, REGISTER_SET_USER_PIPELINE_DATA_LOADED,
@@ -39,6 +40,7 @@ export const defaultState = {
usernameSuggestions: [], usernameSuggestions: [],
validationApiRateLimited: false, validationApiRateLimited: false,
shouldBackupState: false, shouldBackupState: false,
autoGeneratedUsernameExperimentVariation: '',
}; };
const reducer = (state = defaultState, action = {}) => { const reducer = (state = defaultState, action = {}) => {
@@ -55,6 +57,12 @@ const reducer = (state = defaultState, action = {}) => {
registrationFormData: { ...action.payload }, registrationFormData: { ...action.payload },
userPipelineDataLoaded: state.userPipelineDataLoaded, userPipelineDataLoaded: state.userPipelineDataLoaded,
}; };
case REGISTER_SET_AUTO_GENERATED_USERNAME_REGISTRATION_EXP_DATA: {
return {
...state,
autoGeneratedUsernameExperimentVariation: action.payload.autoGeneratedRegExpVariation,
};
}
case REGISTER_NEW_USER.BEGIN: case REGISTER_NEW_USER.BEGIN:
return { return {
...state, ...state,

View File

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

View File

@@ -38,6 +38,7 @@ describe('Registration Reducer Tests', () => {
usernameSuggestions: [], usernameSuggestions: [],
validationApiRateLimited: false, validationApiRateLimited: false,
shouldBackupState: false, shouldBackupState: false,
autoGeneratedUsernameExperimentVariation: '',
}; };
it('should return the initial state', () => { it('should return the initial state', () => {

View File

@@ -43,39 +43,31 @@ export const isFormValid = (
Object.keys(payload).forEach(key => { Object.keys(payload).forEach(key => {
switch (key) { switch (key) {
case 'name': case 'name':
if (!fieldErrors.name) { fieldErrors.name = validateName(payload.name, formatMessage);
fieldErrors.name = validateName(payload.name, formatMessage);
}
if (fieldErrors.name) { isValid = false; } if (fieldErrors.name) { isValid = false; }
break; break;
case 'email': { case 'email': {
if (!fieldErrors.email) { const {
const { fieldError, confirmEmailError, suggestion,
fieldError, confirmEmailError, suggestion, } = validateEmail(payload.email, configurableFormFields?.confirm_email, formatMessage);
} = validateEmail(payload.email, configurableFormFields?.confirm_email, formatMessage); if (fieldError) {
if (fieldError) { fieldErrors.email = fieldError;
fieldErrors.email = fieldError; isValid = false;
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; } if (fieldErrors.email) { isValid = false; }
break; break;
} }
case 'username': case 'username':
if (!fieldErrors.username) { fieldErrors.username = validateUsername(payload.username, formatMessage);
fieldErrors.username = validateUsername(payload.username, formatMessage);
}
if (fieldErrors.username) { isValid = false; } if (fieldErrors.username) { isValid = false; }
break; break;
case 'password': case 'password':
if (!fieldErrors.password) { fieldErrors.password = validatePasswordField(payload.password, formatMessage);
fieldErrors.password = validatePasswordField(payload.password, formatMessage);
}
if (fieldErrors.password) { isValid = false; } if (fieldErrors.password) { isValid = false; }
break; break;
default: default:

View File

@@ -162,11 +162,6 @@ const messages = defineMessages({
defaultMessage: 'Registration using {provider} has timed out.', defaultMessage: 'Registration using {provider} has timed out.',
description: '', 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': { 'registration.tpa.authentication.failure': {
id: 'registration.tpa.authentication.failure', id: 'registration.tpa.authentication.failure',
defaultMessage: 'We are sorry, you are not authorized to access {platform_name} via this channel. ' defaultMessage: 'We are sorry, you are not authorized to access {platform_name} via this channel. '

View File

@@ -18,7 +18,7 @@ import { useNavigate, useParams } from 'react-router-dom';
import { resetPassword, validateToken } from './data/actions'; import { resetPassword, validateToken } from './data/actions';
import { import {
FORM_SUBMISSION_ERROR, PASSWORD_RESET_ERROR, PASSWORD_VALIDATION_ERROR, TOKEN_STATE, FORM_SUBMISSION_ERROR, PASSWORD_RESET_ERROR, PASSWORD_VALIDATION_ERROR, SUCCESS, TOKEN_STATE,
} from './data/constants'; } from './data/constants';
import { resetPasswordResultSelector } from './data/selectors'; import { resetPasswordResultSelector } from './data/selectors';
import { validatePassword } from './data/service'; import { validatePassword } from './data/service';
@@ -30,6 +30,7 @@ import {
LETTER_REGEX, LOGIN_PAGE, NUMBER_REGEX, RESET_PAGE, LETTER_REGEX, LOGIN_PAGE, NUMBER_REGEX, RESET_PAGE,
} from '../data/constants'; } from '../data/constants';
import { getAllPossibleQueryParams, updatePathWithQueryParams, windowScrollTo } from '../data/utils'; import { getAllPossibleQueryParams, updatePathWithQueryParams, windowScrollTo } from '../data/utils';
import { trackPasswordResetSuccess, trackResetPasswordPageViewed } from '../tracking/trackers/reset-password';
const ResetPasswordPage = (props) => { const ResetPasswordPage = (props) => {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
@@ -42,6 +43,15 @@ const ResetPasswordPage = (props) => {
const { token } = useParams(); const { token } = useParams();
const navigate = useNavigate(); const navigate = useNavigate();
useEffect(() => {
if (props.status === TOKEN_STATE.VALID) {
trackResetPasswordPageViewed();
}
if (props.status === SUCCESS) {
trackPasswordResetSuccess();
}
}, [props.status]);
useEffect(() => { useEffect(() => {
if (props.status !== TOKEN_STATE.PENDING && props.status !== PASSWORD_RESET_ERROR) { if (props.status !== TOKEN_STATE.PENDING && props.status !== PASSWORD_RESET_ERROR) {
setErrorCode(props.status); setErrorCode(props.status);
@@ -139,7 +149,7 @@ const ResetPasswordPage = (props) => {
} }
} else if (props.status === PASSWORD_RESET_ERROR) { } else if (props.status === PASSWORD_RESET_ERROR) {
navigate(updatePathWithQueryParams(RESET_PAGE)); navigate(updatePathWithQueryParams(RESET_PAGE));
} else if (props.status === 'success') { } else if (props.status === SUCCESS) {
navigate(updatePathWithQueryParams(LOGIN_PAGE)); navigate(updatePathWithQueryParams(LOGIN_PAGE));
} else { } else {
return ( return (

View File

@@ -1,6 +1,7 @@
import React from 'react';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { configure, IntlProvider } from '@edx/frontend-platform/i18n'; import { configure, injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import { import {
fireEvent, render, screen, fireEvent, render, screen,
} from '@testing-library/react'; } from '@testing-library/react';
@@ -18,6 +19,11 @@ import ResetPasswordPage from '../ResetPasswordPage';
const mockedNavigator = jest.fn(); const mockedNavigator = jest.fn();
const token = '1c-bmjdkc-5e60e084cf8113048ca7'; const token = '1c-bmjdkc-5e60e084cf8113048ca7';
jest.mock('@edx/frontend-platform/analytics', () => ({
sendPageEvent: jest.fn(),
sendTrackEvent: jest.fn(),
}));
jest.mock('@edx/frontend-platform/auth'); jest.mock('@edx/frontend-platform/auth');
jest.mock('react-router-dom', () => ({ jest.mock('react-router-dom', () => ({
...(jest.requireActual('react-router-dom')), ...(jest.requireActual('react-router-dom')),
@@ -25,6 +31,7 @@ jest.mock('react-router-dom', () => ({
useParams: jest.fn().mockReturnValue({ token }), useParams: jest.fn().mockReturnValue({ token }),
})); }));
const IntlResetPasswordPage = injectIntl(ResetPasswordPage);
const mockStore = configureStore(); const mockStore = configureStore();
describe('ResetPasswordPage', () => { describe('ResetPasswordPage', () => {
@@ -93,7 +100,7 @@ describe('ResetPasswordPage', () => {
})); }));
store.dispatch = jest.fn(store.dispatch); store.dispatch = jest.fn(store.dispatch);
render(reduxWrapper(<ResetPasswordPage {...props} />)); render(reduxWrapper(<IntlResetPasswordPage {...props} />));
const newPasswordInput = screen.getByLabelText('New password'); const newPasswordInput = screen.getByLabelText('New password');
const confirmPasswordInput = screen.getByLabelText('Confirm password'); const confirmPasswordInput = screen.getByLabelText('Confirm password');
@@ -118,7 +125,7 @@ describe('ResetPasswordPage', () => {
status: TOKEN_STATE.VALID, status: TOKEN_STATE.VALID,
}, },
}); });
render(reduxWrapper(<ResetPasswordPage {...props} />)); render(reduxWrapper(<IntlResetPasswordPage {...props} />));
const resetPasswordButton = screen.getByRole('button', { name: /Reset password/i, id: 'submit-new-password' }); const resetPasswordButton = screen.getByRole('button', { name: /Reset password/i, id: 'submit-new-password' });
fireEvent.click(resetPasswordButton); fireEvent.click(resetPasswordButton);
@@ -142,7 +149,7 @@ describe('ResetPasswordPage', () => {
status: TOKEN_STATE.VALID, status: TOKEN_STATE.VALID,
}, },
}); });
render(reduxWrapper(<ResetPasswordPage {...props} />)); render(reduxWrapper(<IntlResetPasswordPage {...props} />));
const confirmPasswordInput = screen.getByLabelText('Confirm password'); const confirmPasswordInput = screen.getByLabelText('Confirm password');
fireEvent.change(confirmPasswordInput, { target: { value: 'password-mismatch' } }); fireEvent.change(confirmPasswordInput, { target: { value: 'password-mismatch' } });
@@ -161,7 +168,7 @@ describe('ResetPasswordPage', () => {
}, },
}); });
const { container } = render(reduxWrapper(<ResetPasswordPage {...props} />)); const { container } = render(reduxWrapper(<IntlResetPasswordPage {...props} />));
const alertElements = container.querySelectorAll('.alert-danger'); const alertElements = container.querySelectorAll('.alert-danger');
const rateLimitError = alertElements[0].textContent; const rateLimitError = alertElements[0].textContent;
@@ -177,7 +184,7 @@ describe('ResetPasswordPage', () => {
}, },
}); });
const { container } = render(reduxWrapper(<ResetPasswordPage {...props} />)); const { container } = render(reduxWrapper(<IntlResetPasswordPage {...props} />));
const alertElements = container.querySelectorAll('.alert-danger'); const alertElements = container.querySelectorAll('.alert-danger');
const internalServerError = alertElements[0].textContent; const internalServerError = alertElements[0].textContent;
expect(internalServerError).toBe(validationMessage); expect(internalServerError).toBe(validationMessage);
@@ -186,7 +193,7 @@ describe('ResetPasswordPage', () => {
// ******** miscellaneous tests ******** // ******** miscellaneous tests ********
it('should call validation on password field when blur event fires', () => { it('should call validation on password field when blur event fires', () => {
const resetPasswordPage = render(reduxWrapper(<ResetPasswordPage {...props} />)); const resetPasswordPage = render(reduxWrapper(<IntlResetPasswordPage {...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 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'); const newPasswordInput = resetPasswordPage.container.querySelector('input#newPassword');
newPasswordInput.value = 'test-password'; newPasswordInput.value = 'test-password';
@@ -205,7 +212,7 @@ describe('ResetPasswordPage', () => {
TOKEN_STATE.PENDING, TOKEN_STATE.PENDING,
}; };
render(reduxWrapper(<ResetPasswordPage {...props} />)); render(reduxWrapper(<IntlResetPasswordPage {...props} />));
expect(store.dispatch).toHaveBeenCalledWith(validateToken(token)); expect(store.dispatch).toHaveBeenCalledWith(validateToken(token));
}); });
@@ -214,19 +221,19 @@ describe('ResetPasswordPage', () => {
status: status:
PASSWORD_RESET_ERROR, PASSWORD_RESET_ERROR,
}; };
render(reduxWrapper(<ResetPasswordPage {...props} />)); render(reduxWrapper(<IntlResetPasswordPage {...props} />));
expect(mockedNavigator).toHaveBeenCalledWith(RESET_PAGE); expect(mockedNavigator).toHaveBeenCalledWith(RESET_PAGE);
}); });
it('should redirect the user to root url of the application ', async () => { it('should redirect the user to root url of the application ', async () => {
props = { props = {
status: SUCCESS, status: SUCCESS,
}; };
render(reduxWrapper(<ResetPasswordPage {...props} />)); render(reduxWrapper(<IntlResetPasswordPage {...props} />));
expect(mockedNavigator).toHaveBeenCalledWith(LOGIN_PAGE); expect(mockedNavigator).toHaveBeenCalledWith(LOGIN_PAGE);
}); });
it('shows spinner during token validation', () => { it('shows spinner during token validation', () => {
render(reduxWrapper(<ResetPasswordPage {...props} />)); render(reduxWrapper(<IntlResetPasswordPage {...props} />));
const spinnerElement = document.getElementsByClassName('div.spinner-header'); const spinnerElement = document.getElementsByClassName('div.spinner-header');
expect(spinnerElement).toBeTruthy(); expect(spinnerElement).toBeTruthy();
@@ -235,7 +242,7 @@ describe('ResetPasswordPage', () => {
// ******** redirection tests ******** // ******** redirection tests ********
it('by clicking on sign in tab should redirect onto login page', async () => { it('by clicking on sign in tab should redirect onto login page', async () => {
const { getByText } = render(reduxWrapper(<ResetPasswordPage {...props} />)); const { getByText } = render(reduxWrapper(<IntlResetPasswordPage {...props} />));
const signInTab = getByText('Sign in'); const signInTab = getByText('Sign in');

View File

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

View File

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

View File

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

View File

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

View File

@@ -23,21 +23,21 @@
} }
.alert-link { .alert-link {
color: var(--pgn-color-primary-base) !important; color: $primary !important;
&:hover { &:hover {
text-decoration: underline; text-decoration: underline;
color: var(--pgn-color-info-700) !important; color: $info-700 !important;
} }
} }
} }
.email-suggestion-alert-warning { .email-suggestion-alert-warning {
color: var(--pgn-color-info-500) !important; color: $info-500 !important;
&:hover { &:hover {
text-decoration: underline; text-decoration: underline;
color: var(--pgn-color-info-700) !important; color: $info-700 !important;
} }
} }
@@ -56,7 +56,7 @@
line-height: 24px; line-height: 24px;
font-size: 12px; font-size: 12px;
font-weight: normal; font-weight: normal;
color: var(--pgn-color-primary-700); color: $primary-700;
} }
.username-suggestion--label { .username-suggestion--label {
@@ -65,15 +65,10 @@
margin-right: 0.25rem; margin-right: 0.25rem;
} }
.username__form-group-wrapper {
position: relative;
}
.username-suggestions { .username-suggestions {
position: absolute; position: relative;
inset: 0; margin-top: -2.5rem;
padding-left: 15px; margin-left: 15px;
z-index: 100;
} }
.username-suggestions__close__button { .username-suggestions__close__button {
@@ -81,6 +76,13 @@
position: absolute; position: absolute;
} }
.username-suggestions__error {
position: relative;
margin-top: -13.7%;
margin-bottom: 11%;
margin-left: 15px;
}
.username-scroll-suggested--form-field { .username-scroll-suggested--form-field {
width: 20rem; width: 20rem;
white-space: nowrap; white-space: nowrap;
@@ -97,7 +99,7 @@
} }
} }
@media (--pgn-size-breakpoint-max-width-sm) { @media (max-width: map-get($grid-breakpoints, "sm")) {
.username-scroll-suggested--form-field { .username-scroll-suggested--form-field {
width: 15rem; 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 { .main-content {
padding-top: calc(var(--pgn-spacing-spacer-base) * 1.5) !important; @extend .pt-4;
min-width: 464px !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 { .alert-link {
font-weight: normal; font-weight: normal;
text-decoration: underline; text-decoration: underline;
color: var(--pgn-color-info-300) !important; color: $info-300 !important;
&:hover { &:hover {
color: var(--pgn-color-info-500) !important; color: $info-500 !important;
} }
} }
.form-control { .form-control {
background-color: var(--pgn-color-white) !important; background-color: $white !important;
font-size: 0.875rem; font-size: 0.875rem;
line-height: 1.5; line-height: 1.5;
height: 2.75rem; 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; margin-bottom: 1rem;
font-size: 14px; font-size: 14px;
background-color: var(--pgn-color-white); background-color: $white;
border: 1px solid var(--pgn-color-primary-base); border: 1px solid $primary;
width: 224px; width: 224px;
height: 36px; height: 36px;
color: var(--pgn-color-primary-base); color: $primary;
.btn-tpa__image-icon{ .btn-tpa__image-icon{
background-color: transparent; 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 { .btn-tpa__font-container {
background-color: var(--pgn-color-primary-base); background-color: $primary;
color: var(--pgn-color-white); color: $white;
font-size: 11px; font-size: 11px;
margin-left: -6px; 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 { .btn-oa2-facebook {
color: var(--pgn-color-white); color: $white;
border-color: $facebook-blue; border-color: $facebook-blue;
background-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 { &:focus {
background-color: $facebook-focus-blue; background-color: $facebook-focus-blue;
border: 1px solid $facebook-focus-blue; border: 1px solid $facebook-focus-blue;
color: var(--pgn-color-white); color: $white;
} }
} }
.btn-oa2-google-oauth2 { .btn-oa2-google-oauth2 {
color: var(--pgn-color-white); color: $white;
border-color: $google-blue; border-color: $google-blue;
background-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 { &:focus {
background-color: $google-focus-blue; background-color: $google-focus-blue;
border: 1px solid $google-focus-blue; border: 1px solid $google-focus-blue;
color: var(--pgn-color-white); color: $white;
} }
} }
.btn-oa2-apple-id { .btn-oa2-apple-id {
color: var(--pgn-color-white); color: $white;
border-color: $apple-black; border-color: $apple-black;
background-color: $apple-black; background-color: $apple-black;
font-size: 16px; font-size: 16px;
@@ -190,12 +190,12 @@ $elevation-level-2-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.15);
&:focus { &:focus {
background-color: $apple-focus-black; background-color: $apple-focus-black;
border: 1px solid $apple-focus-black; border: 1px solid $apple-focus-black;
color: var(--pgn-color-white); color: $white;
} }
} }
.btn-oa2-azuread-oauth2 { .btn-oa2-azuread-oauth2 {
color: var(--pgn-color-white); color: $white;
border-color: $microsoft-black; border-color: $microsoft-black;
background-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 { &:focus {
background-color: $microsoft-focus-black; background-color: $microsoft-focus-black;
border: 1px solid $microsoft-focus-black; border: 1px solid $microsoft-focus-black;
color: var(--pgn-color-white); color: $white;
} }
} }
@@ -214,8 +214,9 @@ $elevation-level-2-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.15);
} }
.institute-icon { .institute-icon {
margin: calc(var(--pgn-spacing-spacer-base) * 0.25) !important; @extend .mr-1;
color: var(--pgn-color-gray-base) !important; @extend .text-gray;
display: inline-block; display: inline-block;
margin-bottom: 0.25rem; margin-bottom: 0.25rem;
height: 18px; height: 18px;
@@ -231,7 +232,7 @@ $elevation-level-2-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.15);
} }
.invalid-feedback { .invalid-feedback {
color: var(--pgn-color-red); color: $red;
} }
.full-vertical-height { .full-vertical-height {
@@ -289,22 +290,22 @@ select.form-control {
#password-requirement-left { #password-requirement-left {
opacity: 1; opacity: 1;
font-size: var(--pgn-typography-font-size-xs) !important; @extend .x-small;
filter: drop-shadow($elevation-level-2-shadow) drop-shadow($elevation-level-2-shadow) !important; filter: drop-shadow($elevation-level-2-shadow) drop-shadow($elevation-level-2-shadow) !important;
right: 0.2rem !important; right: 0.2rem !important;
.tooltip-inner { .tooltip-inner {
background: var(--pgn-color-white); background: $white;
display: block; display: block;
color: var(--pgn-color-gray-500); color: $gray-500;
} }
.arrow::before { .arrow::before {
border-left-color: var(--pgn-color-white); border-left-color: $white;
} }
} }
#password-requirement-top { #password-requirement-top {
font-size: var(--pgn-typography-font-size-xs) !important; @extend .x-small;
filter: drop-shadow(var(--pgn-elevation-box-shadow-level-2)) drop-shadow(var(--pgn-elevation-box-shadow-level-2)) !important; filter: drop-shadow($elevation-level-2-shadow) drop-shadow($elevation-level-2-shadow) !important;
opacity: 1; opacity: 1;
width: 90%; width: 90%;
bottom: 10px !important; bottom: 10px !important;
@@ -313,30 +314,30 @@ select.form-control {
.tooltip-inner { .tooltip-inner {
min-width: 464px !important; min-width: 464px !important;
background: var(--pgn-color-white); background: $white;
display: block; display: block;
color: var(--pgn-color-gray-500); color: $gray-500;
} }
.arrow::before { .arrow::before {
border-top-color: var(--pgn-color-white); border-top-color: $white;
} }
} }
.yellow-border { .yellow-border {
border: 2px solid var(--pgn-color-accent-b); border: 2px solid $accent-b;
} }
.institutions__heading { .institutions__heading {
color: var(--pgn-color-primary-700); color: $primary-700;
} }
.logistration-button { .logistration-button {
color: var(--pgn-color-gray-700); color: $gray-700;
} }
.logistration-button:hover{ .logistration-button:hover{
color: var(--pgn-color-gray-700); color: $gray-700;
text-decoration: none; text-decoration: none;
} }
@@ -351,7 +352,7 @@ select.form-control {
width: 2.3rem; width: 2.3rem;
} }
.has-floating-label { .has-floating-label {
color: var(--pgn-color-gray-500); color: $gray-500;
} }
.pgn__form-control-floating-label .pgn__form-control-floating-label-content { .pgn__form-control-floating-label .pgn__form-control-floating-label-content {
@@ -365,7 +366,7 @@ select.form-control {
.form-group__form-field .form-control:focus ~ .pgn__form-control-floating-label .pgn__form-control-floating-label-content { .form-group__form-field .form-control:focus ~ .pgn__form-control-floating-label .pgn__form-control-floating-label-content {
font-size: 16px; font-size: 16px;
color: var(--pgn-color-primary-700); color: $primary-700;
} }
.form-group__form-field .form-control:not([value='']):not(:focus) ~ .form-group__form-field .form-control:not([value='']):not(:focus) ~
@@ -443,14 +444,14 @@ select.form-control {
} }
.table-striped tbody tr:nth-of-type(odd) { .table-striped tbody tr:nth-of-type(odd) {
background-color: var(--pgn-color-light-200); background-color: $light-200;
} }
.institutions--provider-link { .institutions--provider-link {
font-weight: normal; font-weight: normal;
font-size: 0.875rem; font-size: 0.875rem;
line-height: 1.5rem; line-height: 1.5rem;
color: var(--pgn-color-primary-700) color: $primary-700
} }
.pgn__form-control-decorator-trailing { .pgn__form-control-decorator-trailing {

View File

@@ -0,0 +1,22 @@
import { createEventTracker, createPageEventTracker } from '../../data/segment/utils';
export const eventNames = {
loginAndRegistration: 'login_and_registration',
forgotPasswordPageViewed: 'edx.bi.password_reset_form.viewed',
};
export const categories = {
userEngagement: 'user-engagement',
};
// Event tracker for forgot password page viewed
export const trackForgotPasswordPageViewed = () => createEventTracker(
eventNames.forgotPasswordPageViewed,
{
category: categories.userEngagement,
},
)();
export const trackForgotPasswordPageEvent = () => {
createPageEventTracker(eventNames.loginAndRegistration, 'forgot-password')();
};

View File

@@ -0,0 +1,29 @@
import { createEventTracker, createPageEventTracker } from '../../data/segment/utils';
export const eventNames = {
forgotPasswordLinkClicked: 'edx.bi.password-reset_form.toggled',
loginAndRegistration: 'login_and_registration',
registerFormToggled: 'edx.bi.register_form.toggled',
loginSuccess: 'edx.bi.user.account.authenticated.client',
};
export const categories = {
userEngagement: 'user-engagement',
};
// Event tracker for Forgot Password link click
export const trackForgotPasswordLinkClick = () => createEventTracker(
eventNames.forgotPasswordLinkClicked,
{ category: categories.userEngagement },
)();
// Tracks the login page event.
export const trackLoginPageViewed = () => {
createPageEventTracker(eventNames.loginAndRegistration, 'login')();
};
// Tracks the login sucess event.
export const trackLoginSuccess = () => createEventTracker(
eventNames.loginSuccess,
{},
)();

View File

@@ -0,0 +1,37 @@
import { createEventTracker, createPageEventTracker } from '../../data/segment/utils';
export const eventNames = {
progressiveProfilingSubmitClick: 'edx.bi.welcome.page.submit.clicked',
progressiveProfilingSkipLinkClick: 'edx.bi.welcome.page.skip.link.clicked',
disablePostRegistrationRecommendations: 'edx.bi.user.recommendations.not.enabled',
progressiveProfilingSupportLinkCLick: 'edx.bi.welcome.page.support.link.clicked',
loginAndRegistration: 'login_and_registration',
};
// Event link tracker for Progressive profiling skip button click
export const trackProgressiveProfilingSkipLinkClick = evenProperties => createEventTracker(
eventNames.progressiveProfilingSkipLinkClick, { ...evenProperties },
)();
// Event tracker for progressive profiling submit button click
export const trackProgressiveProfilingSubmitClick = (evenProperties) => createEventTracker(
eventNames.progressiveProfilingSubmitClick,
{ ...evenProperties },
)();
// Event tracker for progressive profiling submit button click
export const trackDisablePostRegistrationRecommendations = (evenProperties) => createEventTracker(
eventNames.disablePostRegistrationRecommendations,
{ ...evenProperties },
)();
// Tracks the progressive profiling page event.
export const trackProgressiveProfilingPageViewed = () => {
createPageEventTracker(eventNames.loginAndRegistration, 'welcome')();
};
// Tracks the progressive profiling spport link click.
export const trackProgressiveProfilingSupportLinkCLick = () => createEventTracker(
eventNames.progressiveProfilingSupportLinkCLick,
{},
)();

View File

@@ -0,0 +1,22 @@
import { createEventTracker, createPageEventTracker } from '../../data/segment/utils';
export const eventNames = {
loginAndRegistration: 'login_and_registration',
registrationSuccess: 'edx.bi.user.account.registered.client',
loginFormToggled: 'edx.bi.login_form.toggled',
};
export const categories = {
userEngagement: 'user-engagement',
};
// Event tracker for successful registration
export const trackRegistrationSuccess = () => createEventTracker(
eventNames.registrationSuccess,
{},
)();
// Tracks the progressive profiling page event.
export const trackRegistrationPageViewed = () => {
createPageEventTracker(eventNames.loginAndRegistration, 'register')();
};

View File

@@ -0,0 +1,14 @@
import { createEventTracker, createPageEventTracker } from '../../data/segment/utils';
export const eventNames = {
loginAndRegistration: 'login_and_registration',
resetPasswordSuccess: 'edx.bi.user.password.reset.success',
};
export const trackResetPasswordPageViewed = () => {
createPageEventTracker(eventNames.loginAndRegistration, 'reset-password')();
};
export const trackPasswordResetSuccess = () => {
createEventTracker(eventNames.resetPasswordSuccess, {})();
};

View File

@@ -0,0 +1,37 @@
import { createEventTracker, createPageEventTracker } from '../../../data/segment/utils';
import {
categories,
eventNames,
trackForgotPasswordPageEvent,
trackForgotPasswordPageViewed,
} from '../forgotpassword';
// Mock createEventTracker function
jest.mock('../../../data/segment/utils', () => ({
createEventTracker: jest.fn().mockImplementation(() => jest.fn()),
createPageEventTracker: jest.fn().mockImplementation(() => jest.fn()),
}));
describe('Tracking Functions', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should fire trackForgotPasswordPageEvent', () => {
trackForgotPasswordPageEvent();
expect(createPageEventTracker).toHaveBeenCalledWith(
eventNames.loginAndRegistration,
'forgot-password',
);
});
it('should fire forgotPasswordPageViewedEvent', () => {
trackForgotPasswordPageViewed();
expect(createEventTracker).toHaveBeenCalledWith(
eventNames.forgotPasswordPageViewed,
{ category: categories.userEngagement },
);
});
});

View File

@@ -0,0 +1,37 @@
import { createEventTracker, createPageEventTracker } from '../../../data/segment/utils';
import {
categories,
eventNames,
trackForgotPasswordLinkClick,
trackLoginPageViewed,
} from '../login';
// Mock createEventTracker function
jest.mock('../../../data/segment/utils', () => ({
createEventTracker: jest.fn().mockImplementation(() => jest.fn()),
createPageEventTracker: jest.fn().mockImplementation(() => jest.fn()),
}));
describe('Tracking Functions', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('trackForgotPasswordLinkClick function', () => {
trackForgotPasswordLinkClick();
expect(createEventTracker).toHaveBeenCalledWith(
eventNames.forgotPasswordLinkClicked,
{ category: categories.userEngagement },
);
});
it('trackLoginPageEvent function', () => {
trackLoginPageViewed();
expect(createPageEventTracker).toHaveBeenCalledWith(
eventNames.loginAndRegistration,
'login',
);
});
});

View File

@@ -0,0 +1,37 @@
import { createEventTracker, createPageEventTracker } from '../../../data/segment/utils';
import {
eventNames,
trackProgressiveProfilingPageViewed,
trackProgressiveProfilingSkipLinkClick,
} from '../progressive-profiling';
// Mock createEventTracker function
jest.mock('../../../data/segment/utils', () => ({
createEventTracker: jest.fn().mockImplementation(() => jest.fn()),
createPageEventTracker: jest.fn().mockImplementation(() => jest.fn()),
createLinkTracker: jest.fn().mockImplementation(() => jest.fn()),
}));
describe('Tracking Functions', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should fire trackProgressiveProfilingSkipLinkClickEvent', () => {
trackProgressiveProfilingSkipLinkClick();
expect(createEventTracker).toHaveBeenCalledWith(
eventNames.progressiveProfilingSkipLinkClick,
{},
);
});
it('should fire trackProgressiveProfilingPageEvent', () => {
trackProgressiveProfilingPageViewed();
expect(createPageEventTracker).toHaveBeenCalledWith(
eventNames.loginAndRegistration,
'welcome',
);
});
});

View File

@@ -0,0 +1,36 @@
import { createEventTracker, createPageEventTracker } from '../../../data/segment/utils';
import {
eventNames,
trackRegistrationPageViewed,
trackRegistrationSuccess,
} from '../register';
// Mock createEventTracker function
jest.mock('../../../data/segment/utils', () => ({
createEventTracker: jest.fn().mockImplementation(() => jest.fn()),
createPageEventTracker: jest.fn().mockImplementation(() => jest.fn()),
}));
describe('Tracking Functions', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should fire registrationSuccessEvent', () => {
trackRegistrationSuccess();
expect(createEventTracker).toHaveBeenCalledWith(
eventNames.registrationSuccess,
{},
);
});
it('should fire trackRegistrationPageEvent', () => {
trackRegistrationPageViewed();
expect(createPageEventTracker).toHaveBeenCalledWith(
eventNames.loginAndRegistration,
'register',
);
});
});

View File

@@ -0,0 +1,26 @@
import { createPageEventTracker } from '../../../data/segment/utils';
import {
eventNames,
trackResetPasswordPageViewed,
} from '../reset-password';
// Mock createEventTracker function
jest.mock('../../../data/segment/utils', () => ({
createEventTracker: jest.fn().mockImplementation(() => jest.fn()),
createPageEventTracker: jest.fn().mockImplementation(() => jest.fn()),
}));
describe('Tracking Functions', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should fire trackResettPasswordPageEvent', () => {
trackResetPasswordPageViewed();
expect(createPageEventTracker).toHaveBeenCalledWith(
eventNames.loginAndRegistration,
'reset-password',
);
});
});