Compare commits
164 Commits
release/te
...
2u-main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd02f8b3f4 | ||
|
|
5ba6adcd7d | ||
|
|
527bc5aa37 | ||
|
|
fd5122bc10 | ||
|
|
97c9a09efa | ||
|
|
cf3e8b5c7f | ||
|
|
764d5f51e1 | ||
|
|
c4ef3dbbd9 | ||
|
|
f8a5cb50ed | ||
|
|
b97f777b6f | ||
|
|
b2f7579054 | ||
|
|
24742c1cf5 | ||
|
|
051383e68a | ||
|
|
5443ebd01b | ||
|
|
3aa2422735 | ||
|
|
90a7dfeb15 | ||
|
|
a552d025b6 | ||
|
|
97c7bd744f | ||
|
|
55c5f705fb | ||
|
|
f4e2adc261 | ||
|
|
58ec90aca6 | ||
|
|
76e400f0ad | ||
|
|
5bd6926f2f | ||
|
|
43a584ebd1 | ||
|
|
4cf0a64d81 | ||
|
|
db3d007c51 | ||
|
|
55a930840f | ||
|
|
fad82b52ad | ||
|
|
41450686aa | ||
|
|
2fcda640f5 | ||
|
|
82252f9a7c | ||
|
|
818d0278a5 | ||
|
|
2cb62ca6d4 | ||
|
|
ff3fce99db | ||
|
|
157c302384 | ||
|
|
f2a905d373 | ||
|
|
e984a0b07b | ||
|
|
7150d4562a | ||
|
|
451056866f | ||
|
|
76f0cc54d9 | ||
|
|
fb70f7a1c2 | ||
|
|
b664150b4d | ||
|
|
da5a2e31b6 | ||
|
|
486d0bfd37 | ||
|
|
9332fc113a | ||
|
|
181e837ca4 | ||
|
|
735a9afc3c | ||
|
|
319c48f1c8 | ||
|
|
fbd73bfbfe | ||
|
|
27a63cf406 | ||
|
|
7ea351f6a0 | ||
|
|
61e8c254d7 | ||
|
|
3a08e790c3 | ||
|
|
b4c5171886 | ||
|
|
7b83c416f8 | ||
|
|
a3c261bb13 | ||
|
|
98d03aa29f | ||
|
|
f5d5e2fd02 | ||
|
|
490bf27ed1 | ||
|
|
780acac2fd | ||
|
|
2ea763701d | ||
|
|
e2d9ba5857 | ||
|
|
747d656f0a | ||
|
|
8638ed5cf4 | ||
|
|
ca2e7f554a | ||
|
|
f3f14fb3e7 | ||
|
|
6c8b3835b6 | ||
|
|
8cbe6ce02e | ||
|
|
a53334d3bf | ||
|
|
0172c79dd9 | ||
|
|
5d4abcbab3 | ||
|
|
c9987eb2f4 | ||
|
|
a2ad9d5248 | ||
|
|
cca87bd16a | ||
|
|
206c4c887b | ||
|
|
09dc21eb0e | ||
|
|
945af2bdfd | ||
|
|
0dca6f3fdc | ||
|
|
189a67c9dc | ||
|
|
3dceb63b9c | ||
|
|
9385174b93 | ||
|
|
86ed8e2361 | ||
|
|
f5a6ece6b1 | ||
|
|
0d71e31ffb | ||
|
|
38dd2944b8 | ||
|
|
f4708ed274 | ||
|
|
cb7300441c | ||
|
|
244b9e68e6 | ||
|
|
c04ed9aa43 | ||
|
|
354c73bb2a | ||
|
|
76a5a5dffa | ||
|
|
5efe9d8344 | ||
|
|
cd2003921b | ||
|
|
7339aec7c2 | ||
|
|
025870a3b9 | ||
|
|
8dc77d5db6 | ||
|
|
40a1f4ce6b | ||
|
|
bb9fcd91c0 | ||
|
|
0fa00290da | ||
|
|
b18caa2da0 | ||
|
|
5ca86f9183 | ||
|
|
2a9dbe9d30 | ||
|
|
62508e3bc7 | ||
|
|
ceb489753b | ||
|
|
5035a07e0a | ||
|
|
f086a165e2 | ||
|
|
9239df3620 | ||
|
|
009125c3ef | ||
|
|
b69ed6e422 | ||
|
|
07ee2392e9 | ||
|
|
2bfce01772 | ||
|
|
1477ed33d7 | ||
|
|
c4f1a97316 | ||
|
|
47b0501e1c | ||
|
|
e496bb62c5 | ||
|
|
b41fca3605 | ||
|
|
ac2548913f | ||
|
|
cd9b3bd084 | ||
|
|
efc07aac67 | ||
|
|
2d50ed224f | ||
|
|
d10f9b932b | ||
|
|
05aa85a5fb | ||
|
|
56bd6d835e | ||
|
|
afd4d24360 | ||
|
|
4898864416 | ||
|
|
739f94d624 | ||
|
|
1819edc9b7 | ||
|
|
ad0d75ab0d | ||
|
|
a90ebb7d4d | ||
|
|
f8290adab5 | ||
|
|
788a42b341 | ||
|
|
4f48e82959 | ||
|
|
99850574fb | ||
|
|
d66afe98f0 | ||
|
|
e2cdfce832 | ||
|
|
c1e63da778 | ||
|
|
ecf4c3ae53 | ||
|
|
2428b4c389 | ||
|
|
099fe8d717 | ||
|
|
4755540be8 | ||
|
|
9a30f053c7 | ||
|
|
6b983e18d3 | ||
|
|
327210192c | ||
|
|
0d603b5fa1 | ||
|
|
efaa83a1bc | ||
|
|
bd63bb1f15 | ||
|
|
5754c2961a | ||
|
|
dcbd644a25 | ||
|
|
52e438652c | ||
|
|
d8947a4c0a | ||
|
|
03d1666c2c | ||
|
|
3782503983 | ||
|
|
b219fe3683 | ||
|
|
90f650ce3e | ||
|
|
6f325c20c3 | ||
|
|
de12dfbf9e | ||
|
|
c663f6fa30 | ||
|
|
dba93333fd | ||
|
|
611af07326 | ||
|
|
564ec70d9e | ||
|
|
65e95a4d1b | ||
|
|
cf2b50005b | ||
|
|
faf4ff8488 | ||
|
|
7d64220852 |
6
.env
6
.env
@@ -16,6 +16,9 @@ SITE_NAME=null
|
||||
INFO_EMAIL=''
|
||||
# ***** Cookies *****
|
||||
USER_RETENTION_COOKIE_NAME=null
|
||||
# ***** Cohesion Keys *****
|
||||
COHESION_WRITE_KEY=''
|
||||
COHESION_SOURCE_KEY=''
|
||||
# ***** Links *****
|
||||
LOGIN_ISSUE_SUPPORT_LINK=''
|
||||
AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK=null
|
||||
@@ -41,3 +44,6 @@ BANNER_IMAGE_EXTRA_SMALL=''
|
||||
# ***** Miscellaneous *****
|
||||
APP_ID=''
|
||||
MFE_CONFIG_API_URL=''
|
||||
# Fallback in local style files
|
||||
PARAGON_THEME_URLS={}
|
||||
RECAPTCHA_SITE_KEY_WEB=''
|
||||
|
||||
@@ -25,6 +25,9 @@ ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN='true'
|
||||
# ***** Cookies *****
|
||||
SESSION_COOKIE_DOMAIN='localhost'
|
||||
USER_INFO_COOKIE_NAME='edx-user-info'
|
||||
# ***** Cohesion Keys *****
|
||||
COHESION_WRITE_KEY=''
|
||||
COHESION_SOURCE_KEY=''
|
||||
# ***** Links *****
|
||||
LOGIN_ISSUE_SUPPORT_LINK='http://localhost:18000/login-issue-support-url'
|
||||
TOS_AND_HONOR_CODE='http://localhost:18000/honor'
|
||||
@@ -41,3 +44,6 @@ APP_ID=''
|
||||
MFE_CONFIG_API_URL=''
|
||||
ZENDESK_KEY=''
|
||||
ZENDESK_LOGO_URL=''
|
||||
# Fallback in local style files
|
||||
PARAGON_THEME_URLS={}
|
||||
RECAPTCHA_SITE_KEY_WEB=''
|
||||
|
||||
@@ -18,3 +18,7 @@ SEGMENT_KEY=''
|
||||
SITE_NAME='Your Platform Name Here'
|
||||
APP_ID=''
|
||||
MFE_CONFIG_API_URL=''
|
||||
COHESION_WRITE_KEY=''
|
||||
COHESION_SOURCE_KEY=''
|
||||
PARAGON_THEME_URLS={}
|
||||
RECAPTCHA_SITE_KEY_WEB=''
|
||||
|
||||
13
.eslintrc.js
13
.eslintrc.js
@@ -1,7 +1,7 @@
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
const { createConfig } = require('@openedx/frontend-build');
|
||||
|
||||
module.exports = createConfig('eslint', {
|
||||
const config = createConfig('eslint', {
|
||||
rules: {
|
||||
// Temporarily update the 'indent', 'template-curly-spacing' and
|
||||
// 'no-multiple-empty-lines' rules since they are causing eslint
|
||||
@@ -50,3 +50,14 @@ module.exports = createConfig('eslint', {
|
||||
'function-paren-newline': 'off',
|
||||
},
|
||||
});
|
||||
|
||||
config.settings = {
|
||||
'import/resolver': {
|
||||
node: {
|
||||
paths: ['src', 'node_modules'],
|
||||
extensions: ['.js', '.jsx'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
# The following users are the owners of all frontend-app-authn files
|
||||
* @openedx/2u-infinity
|
||||
60
example.env.config.js
Normal file
60
example.env.config.js
Normal file
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
Authn MFE is now able to handle JS-based configuration!
|
||||
|
||||
For the time being, the `.env.*` files are still made available when cloning down this repo or pulling from
|
||||
the master branch. To switch to using `env.config.js`, make a copy of `example.env.config.js` and configure as needed.
|
||||
|
||||
For testing with Jest Snapshot, there is a mock in `/src/setupTest.jsx` for `getConfig` that will need to be
|
||||
uncommented.
|
||||
|
||||
Note: having both .env and env.config.js files will follow a predictable order, in which non-empty values in the
|
||||
JS-based config will overwrite the .env environment variables.
|
||||
|
||||
frontend-platform's getConfig loads configuration in the following sequence:
|
||||
- .env file config
|
||||
- optional handlers (commonly used to merge MFE-specific config in via additional process.env variables)
|
||||
- env.config.js file config
|
||||
- runtime config
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
NODE_ENV: 'development',
|
||||
NODE_PATH: './src',
|
||||
PORT: 1999,
|
||||
ACCESS_TOKEN_COOKIE_NAME: 'edx-jwt-cookie-header-payload',
|
||||
BASE_URL: 'http://localhost:1999',
|
||||
CREDENTIALS_BASE_URL: 'http://localhost:18150',
|
||||
CSRF_TOKEN_API_PATH: '/csrf/api/v1/token',
|
||||
ECOMMERCE_BASE_URL: 'http://localhost:18130',
|
||||
LANGUAGE_PREFERENCE_COOKIE_NAME: 'openedx-language-preference',
|
||||
LMS_BASE_URL: 'http://localhost:18000',
|
||||
LOGIN_URL: 'http://localhost:1999/login',
|
||||
LOGOUT_URL: 'http://localhost:18000/logout',
|
||||
LOGO_URL: 'https://edx-cdn.org/v3/default/logo.svg',
|
||||
LOGO_TRADEMARK_URL: 'https://edx-cdn.org/v3/default/logo-trademark.svg',
|
||||
LOGO_WHITE_URL: 'https://edx-cdn.org/v3/default/logo-white.svg',
|
||||
FAVICON_URL: 'https://edx-cdn.org/v3/default/favicon.ico',
|
||||
MARKETING_SITE_BASE_URL: 'http://localhost:18000',
|
||||
ORDER_HISTORY_URL: 'http://localhost:1996/orders',
|
||||
REFRESH_ACCESS_TOKEN_ENDPOINT: 'http://localhost:18000/login_refresh',
|
||||
SEGMENT_KEY: '',
|
||||
SITE_NAME: 'Your Platform Name Here',
|
||||
INFO_EMAIL: 'info@example.com',
|
||||
ENABLE_DYNAMIC_REGISTRATION_FIELDS: 'true',
|
||||
ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN: 'true',
|
||||
SESSION_COOKIE_DOMAIN: 'localhost',
|
||||
USER_INFO_COOKIE_NAME: 'edx-user-info',
|
||||
LOGIN_ISSUE_SUPPORT_LINK: 'http://localhost:18000/login-issue-support-url',
|
||||
TOS_AND_HONOR_CODE: 'http://localhost:18000/honor',
|
||||
TOS_LINK: 'http://localhost:18000/tos',
|
||||
PRIVACY_POLICY: 'http://localhost:18000/privacy',
|
||||
AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK: 'http://localhost:1999/welcome',
|
||||
BANNER_IMAGE_LARGE: '',
|
||||
BANNER_IMAGE_MEDIUM: '',
|
||||
BANNER_IMAGE_SMALL: '',
|
||||
BANNER_IMAGE_EXTRA_SMALL: '',
|
||||
APP_ID: '',
|
||||
MFE_CONFIG_API_URL: '',
|
||||
ZENDESK_KEY: '',
|
||||
ZENDESK_LOGO_URL: '',
|
||||
};
|
||||
14026
package-lock.json
generated
14026
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
29
package.json
29
package.json
@@ -35,27 +35,30 @@
|
||||
"@fortawesome/fontawesome-svg-core": "6.7.2",
|
||||
"@fortawesome/free-brands-svg-icons": "6.7.2",
|
||||
"@fortawesome/free-solid-svg-icons": "6.7.2",
|
||||
"@fortawesome/react-fontawesome": "0.2.2",
|
||||
"@openedx/paragon": "^22.16.0",
|
||||
"@openedx/frontend-plugin-framework": "^1.3.0",
|
||||
"@fortawesome/react-fontawesome": "0.2.6",
|
||||
"@openedx/paragon": "^23.4.2",
|
||||
"@optimizely/react-sdk": "^2.9.1",
|
||||
"@redux-devtools/extension": "3.3.0",
|
||||
"@testing-library/react": "^16.2.0",
|
||||
"algoliasearch": "^4.14.3",
|
||||
"algoliasearch-helper": "^3.14.0",
|
||||
"algoliasearch-helper": "^3.26.0",
|
||||
"classnames": "2.5.1",
|
||||
"core-js": "3.41.0",
|
||||
"core-js": "3.43.0",
|
||||
"fastest-levenshtein": "1.0.16",
|
||||
"form-urlencoded": "6.1.5",
|
||||
"form-urlencoded": "6.1.6",
|
||||
"prop-types": "15.8.1",
|
||||
"query-string": "7.1.3",
|
||||
"react-error-boundary": "^4.0.13",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-google-recaptcha-v3": "^1.11.0",
|
||||
"react-helmet": "6.1.0",
|
||||
"react-loading-skeleton": "3.5.0",
|
||||
"react-redux": "7.2.9",
|
||||
"react-responsive": "8.2.0",
|
||||
"react-router": "6.30.0",
|
||||
"react-router-dom": "6.30.0",
|
||||
"react-router": "6.30.1",
|
||||
"react-router-dom": "6.30.1",
|
||||
"react-zendesk": "^0.1.13",
|
||||
"redux": "4.2.1",
|
||||
"redux-logger": "3.0.6",
|
||||
@@ -69,12 +72,14 @@
|
||||
"devDependencies": {
|
||||
"@edx/browserslist-config": "^1.1.1",
|
||||
"@edx/reactifex": "1.1.0",
|
||||
"@openedx/frontend-build": "^14.4.2",
|
||||
"babel-plugin-formatjs": "10.5.37",
|
||||
"eslint-plugin-import": "2.31.0",
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"@openedx/frontend-build": "^14.6.2",
|
||||
"babel-plugin-formatjs": "10.5.39",
|
||||
"eslint-plugin-import": "2.32.0",
|
||||
"glob": "7.2.3",
|
||||
"history": "5.3.0",
|
||||
"jest": "29.7.0",
|
||||
"react-test-renderer": "^18.3.1"
|
||||
"jest": "30.1.3",
|
||||
"react-test-renderer": "^18.3.1",
|
||||
"ts-jest": "^29.4.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,32 @@
|
||||
<!doctype html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en-us">
|
||||
<head>
|
||||
<title><%= (process.env.SITE_NAME && process.env.SITE_NAME != 'null') ? 'Authentication | ' + process.env.SITE_NAME : 'Authentication' %></title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="shortcut icon" href="<%=htmlWebpackPlugin.options.FAVICON_URL%>" type="image/x-icon"/>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/iframe-resizer/4.4.4/iframeResizer.contentWindow.min.js"
|
||||
integrity="sha512-IWwZFBvHzN41wNI6etRLLuLrDDj/6AwJcPt7cmKJAzluYTIHHQ1PF8wh0rSy05jxEvvjflVvH2MxeV6riyEEXg=="
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer">
|
||||
</script>
|
||||
<% if (process.env.OPTIMIZELY_URL) { %>
|
||||
<script
|
||||
src="<%= process.env.OPTIMIZELY_URL %>"
|
||||
></script>
|
||||
<% } else if (process.env.OPTIMIZELY_PROJECT_ID) { %>
|
||||
<script
|
||||
src="<%= process.env.MARKETING_SITE_BASE_URL %>/optimizelyjs/<%= process.env.OPTIMIZELY_PROJECT_ID %>.js"
|
||||
></script>
|
||||
<% } %>
|
||||
<title>
|
||||
<%= (process.env.SITE_NAME && process.env.SITE_NAME !='null' ) ?
|
||||
'Authentication | ' + process.env.SITE_NAME : 'Authentication' %>
|
||||
</title>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<link
|
||||
rel="shortcut icon"
|
||||
href="<%=htmlWebpackPlugin.options.FAVICON_URL%>"
|
||||
type="image/x-icon"
|
||||
/>
|
||||
<script defer src="https://www.edx.org/beam-wrapper.js" ></script>
|
||||
<script
|
||||
src="https://cdnjs.cloudflare.com/ajax/libs/iframe-resizer/4.4.4/iframeResizer.contentWindow.min.js"
|
||||
integrity="sha512-IWwZFBvHzN41wNI6etRLLuLrDDj/6AwJcPt7cmKJAzluYTIHHQ1PF8wh0rSy05jxEvvjflVvH2MxeV6riyEEXg=="
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
></script>
|
||||
<% if (process.env.OPTIMIZELY_URL) { %>
|
||||
<script src="<%= process.env.OPTIMIZELY_URL %>"></script>
|
||||
<% } else if (process.env.OPTIMIZELY_PROJECT_ID) { %>
|
||||
<script src="<%= process.env.MARKETING_SITE_BASE_URL %>/optimizelyjs/<%= process.env.OPTIMIZELY_PROJECT_ID %>.js"></script>
|
||||
<% } %>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
|
||||
2
public/robots.txt
Normal file
2
public/robots.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Disallow: /
|
||||
@@ -2,11 +2,12 @@ import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import { GoogleReCaptchaProvider } from 'react-google-recaptcha-v3';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
EmbeddedRegistrationRoute, NotFoundPage, registerIcons, UnAuthOnlyRoute, Zendesk,
|
||||
EmbeddedRegistrationRoute, NotFoundPage, registerIcons, RouteTracker, UnAuthOnlyRoute,
|
||||
} from './common-components';
|
||||
import configureStore from './data/configureStore';
|
||||
import {
|
||||
@@ -22,6 +23,7 @@ import {
|
||||
import { updatePathWithQueryParams } from './data/utils';
|
||||
import { ForgotPasswordPage } from './forgot-password';
|
||||
import Logistration from './logistration/Logistration';
|
||||
import MainAppSlot from './plugin-slots/MainAppSlot';
|
||||
import { ProgressiveProfiling } from './progressive-profiling';
|
||||
import { RecommendationsPage } from './recommendations';
|
||||
import { RegistrationPage } from './register';
|
||||
@@ -31,33 +33,43 @@ import './index.scss';
|
||||
|
||||
registerIcons();
|
||||
|
||||
const MainApp = () => (
|
||||
<AppProvider store={configureStore()}>
|
||||
<Helmet>
|
||||
<link rel="shortcut icon" href={getConfig().FAVICON_URL} type="image/x-icon" />
|
||||
</Helmet>
|
||||
{getConfig().ZENDESK_KEY && <Zendesk />}
|
||||
<Routes>
|
||||
<Route path="/" element={<Navigate replace to={updatePathWithQueryParams(REGISTER_PAGE)} />} />
|
||||
<Route
|
||||
path={REGISTER_EMBEDDED_PAGE}
|
||||
element={<EmbeddedRegistrationRoute><RegistrationPage /></EmbeddedRegistrationRoute>}
|
||||
/>
|
||||
<Route
|
||||
path={LOGIN_PAGE}
|
||||
element={
|
||||
<UnAuthOnlyRoute><Logistration selectedPage={LOGIN_PAGE} /></UnAuthOnlyRoute>
|
||||
}
|
||||
/>
|
||||
<Route path={REGISTER_PAGE} element={<UnAuthOnlyRoute><Logistration /></UnAuthOnlyRoute>} />
|
||||
<Route path={RESET_PAGE} element={<UnAuthOnlyRoute><ForgotPasswordPage /></UnAuthOnlyRoute>} />
|
||||
<Route path={PASSWORD_RESET_CONFIRM} element={<ResetPasswordPage />} />
|
||||
<Route path={AUTHN_PROGRESSIVE_PROFILING} element={<ProgressiveProfiling />} />
|
||||
<Route path={RECOMMENDATIONS} element={<RecommendationsPage />} />
|
||||
<Route path={PAGE_NOT_FOUND} element={<NotFoundPage />} />
|
||||
<Route path="*" element={<Navigate replace to={PAGE_NOT_FOUND} />} />
|
||||
</Routes>
|
||||
</AppProvider>
|
||||
);
|
||||
const MainApp = () => {
|
||||
const recaptchaKey = getConfig().RECAPTCHA_SITE_KEY_WEB;
|
||||
|
||||
return (
|
||||
<GoogleReCaptchaProvider
|
||||
reCaptchaKey={recaptchaKey}
|
||||
useEnterprise
|
||||
>
|
||||
<AppProvider store={configureStore()}>
|
||||
<Helmet>
|
||||
<link rel="shortcut icon" href={getConfig().FAVICON_URL} type="image/x-icon" />
|
||||
</Helmet>
|
||||
<Routes>
|
||||
<Route path="/" element={<Navigate replace to={updatePathWithQueryParams(REGISTER_PAGE)} />} />
|
||||
<Route
|
||||
path={REGISTER_EMBEDDED_PAGE}
|
||||
element={<EmbeddedRegistrationRoute><RegistrationPage /></EmbeddedRegistrationRoute>}
|
||||
/>
|
||||
<Route
|
||||
path={LOGIN_PAGE}
|
||||
element={
|
||||
<UnAuthOnlyRoute><Logistration selectedPage={LOGIN_PAGE} /></UnAuthOnlyRoute>
|
||||
}
|
||||
/>
|
||||
<Route path={REGISTER_PAGE} element={<UnAuthOnlyRoute><Logistration /></UnAuthOnlyRoute>} />
|
||||
<Route path={RESET_PAGE} element={<UnAuthOnlyRoute><ForgotPasswordPage /></UnAuthOnlyRoute>} />
|
||||
<Route path={PASSWORD_RESET_CONFIRM} element={<ResetPasswordPage />} />
|
||||
<Route path={AUTHN_PROGRESSIVE_PROFILING} element={<ProgressiveProfiling />} />
|
||||
<Route path={RECOMMENDATIONS} element={<RecommendationsPage />} />
|
||||
<Route path={PAGE_NOT_FOUND} element={<NotFoundPage />} />
|
||||
<Route path="*" element={<Navigate replace to={PAGE_NOT_FOUND} />} />
|
||||
</Routes>
|
||||
<RouteTracker />
|
||||
<MainAppSlot />
|
||||
</AppProvider>
|
||||
</GoogleReCaptchaProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default MainApp;
|
||||
|
||||
22
src/cohesion/constants.js
Normal file
22
src/cohesion/constants.js
Normal file
@@ -0,0 +1,22 @@
|
||||
export const PAGE_TYPES = {
|
||||
ACCOUNT_CREATION: 'account-creation',
|
||||
SIGN_IN: 'sign-in',
|
||||
};
|
||||
|
||||
export const ELEMENT_TYPES = {
|
||||
BUTTON: 'BUTTON',
|
||||
};
|
||||
|
||||
export const EVENT_TYPES = { ElementClicked: 'redventures.usertracking.v3.ElementClicked' };
|
||||
|
||||
export const ELEMENT_TEXT = {
|
||||
CREATE_ACCOUNT: 'create-account',
|
||||
OPT_IN_TEXT: 'I agree that edx may send me marketing messages',
|
||||
SIGN_IN: 'Sign In',
|
||||
};
|
||||
|
||||
export const ELEMENT_NAME = {
|
||||
SIGN_IN: PAGE_TYPES.SIGN_IN,
|
||||
OPT_OUT: 'opt-out',
|
||||
CREATE_ACCOUNT: 'Create an account for free',
|
||||
};
|
||||
6
src/cohesion/data/actions.js
Normal file
6
src/cohesion/data/actions.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export const SET_COHESION_EVENT_ELEMENT_STATES = 'SET_COHESION_EVENT_ELEMENT_STATES';
|
||||
|
||||
export const setCohesionEventStates = (eventData) => ({
|
||||
type: SET_COHESION_EVENT_ELEMENT_STATES,
|
||||
payload: eventData,
|
||||
});
|
||||
17
src/cohesion/data/reducers.js
Normal file
17
src/cohesion/data/reducers.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import { SET_COHESION_EVENT_ELEMENT_STATES } from './actions';
|
||||
|
||||
export const storeName = 'cohesion';
|
||||
|
||||
export const defaultState = {
|
||||
eventData: {},
|
||||
};
|
||||
|
||||
export const reducer = (state = defaultState, action = {}) => {
|
||||
if (action.type === SET_COHESION_EVENT_ELEMENT_STATES) {
|
||||
return {
|
||||
...state,
|
||||
eventData: action.payload,
|
||||
};
|
||||
}
|
||||
return state;
|
||||
};
|
||||
24
src/cohesion/trackers.js
Normal file
24
src/cohesion/trackers.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import { EVENT_TYPES } from './constants';
|
||||
|
||||
/**
|
||||
* Tracks cohesion events by setting the page type and tracking a click event.
|
||||
*
|
||||
* @param {string} pageType - The type of page where the event occurred.
|
||||
* @param {string} elementType - The type of the web element (e.g., 'BUTTON', 'LINK').
|
||||
* @param {string} webElementText - The text content of the web element.
|
||||
* @param {string} webElementName - The name of the web element.
|
||||
*/
|
||||
const trackCohesionEvent = (eventData) => {
|
||||
window.chsn_pageType = eventData.pageType;
|
||||
const webElement = {
|
||||
elementType: eventData.elementType,
|
||||
text: eventData.webElementText,
|
||||
name: eventData.webElementName,
|
||||
};
|
||||
window.tagular?.('beam', {
|
||||
'@type': EVENT_TYPES.ElementClicked,
|
||||
webElement,
|
||||
});
|
||||
};
|
||||
|
||||
export default trackCohesionEvent;
|
||||
6
src/cohesion/utils.js
Normal file
6
src/cohesion/utils.js
Normal file
@@ -0,0 +1,6 @@
|
||||
const mockTagular = () => {
|
||||
const getTagular = jest.fn();
|
||||
window.tagular = getTagular;
|
||||
};
|
||||
|
||||
export default mockTagular;
|
||||
@@ -60,7 +60,7 @@ const InstitutionLogistration = props => {
|
||||
className="btn nav-item p-0 mb-1 institutions--provider-link"
|
||||
destination={lmsBaseUrl + provider.loginUrl}
|
||||
>
|
||||
{provider.name}
|
||||
{provider?.name}
|
||||
</Hyperlink>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
|
||||
import trackCohesionEvent from '../cohesion/trackers';
|
||||
import {
|
||||
AUTHN_PROGRESSIVE_PROFILING, RECOMMENDATIONS, REDIRECT,
|
||||
} from '../data/constants';
|
||||
import { setCookie } from '../data/utils';
|
||||
import setCookie from '../data/utils/cookies';
|
||||
import { redirectWithDelay } from '../data/utils/dataUtils';
|
||||
|
||||
const RedirectLogistration = (props) => {
|
||||
const {
|
||||
@@ -20,10 +24,16 @@ const RedirectLogistration = (props) => {
|
||||
userId,
|
||||
registrationEmbedded,
|
||||
host,
|
||||
currectProvider,
|
||||
} = props;
|
||||
const cohesionEventData = useSelector(state => state.cohesion.eventData);
|
||||
let finalRedirectUrl = '';
|
||||
|
||||
if (success) {
|
||||
// This event is used by cohesion upon successful login and registration
|
||||
if (!currectProvider) {
|
||||
trackCohesionEvent(cohesionEventData);
|
||||
}
|
||||
// If we're in a third party auth pipeline, we must complete the pipeline
|
||||
// once user has successfully logged in. Otherwise, redirect to the specified redirect url.
|
||||
// Note: For multiple enterprise use case, we need to make sure that user first visits the
|
||||
@@ -75,8 +85,7 @@ const RedirectLogistration = (props) => {
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
window.location.href = finalRedirectUrl;
|
||||
redirectWithDelay(finalRedirectUrl);
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -94,6 +103,7 @@ RedirectLogistration.defaultProps = {
|
||||
userId: null,
|
||||
registrationEmbedded: false,
|
||||
host: '',
|
||||
currectProvider: '',
|
||||
};
|
||||
|
||||
RedirectLogistration.propTypes = {
|
||||
@@ -108,6 +118,7 @@ RedirectLogistration.propTypes = {
|
||||
userId: PropTypes.number,
|
||||
registrationEmbedded: PropTypes.bool,
|
||||
host: PropTypes.string,
|
||||
currectProvider: PropTypes.string,
|
||||
};
|
||||
|
||||
export default RedirectLogistration;
|
||||
|
||||
15
src/common-components/RouteTracker.jsx
Normal file
15
src/common-components/RouteTracker.jsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
const RouteTracker = () => {
|
||||
const location = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
window.tagular?.('pageView');
|
||||
}, [location]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default RouteTracker;
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
@@ -8,17 +9,35 @@ import { Login } from '@openedx/paragon/icons';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import messages from './messages';
|
||||
import { LOGIN_PAGE, SUPPORTED_ICON_CLASSES } from '../data/constants';
|
||||
import { ELEMENT_TYPES, PAGE_TYPES } from '../cohesion/constants';
|
||||
import trackCohesionEvent from '../cohesion/trackers';
|
||||
import {
|
||||
LOGIN_PAGE, REGISTER_PAGE, SUPPORTED_ICON_CLASSES,
|
||||
} from '../data/constants';
|
||||
import { setCookie } from '../data/utils';
|
||||
import { redirectWithDelay } from '../data/utils/dataUtils';
|
||||
|
||||
const SocialAuthProviders = (props) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { referrer, socialAuthProviders } = props;
|
||||
const registrationFields = useSelector(state => state.register.registrationFormData);
|
||||
|
||||
function handleSubmit(e) {
|
||||
function handleSubmit(e, providerName) {
|
||||
e.preventDefault();
|
||||
const eventData = {
|
||||
pageType: referrer === LOGIN_PAGE ? PAGE_TYPES.SIGN_IN : PAGE_TYPES.ACCOUNT_CREATION,
|
||||
elementType: ELEMENT_TYPES.BUTTON,
|
||||
webElementText: providerName,
|
||||
webElementName: providerName.toLowerCase(),
|
||||
};
|
||||
// This event is used by cohesion upon successful login
|
||||
trackCohesionEvent(eventData);
|
||||
|
||||
if (referrer === REGISTER_PAGE) {
|
||||
setCookie('marketingEmailsOptIn', registrationFields?.configurableFormFields?.marketingEmailsOptIn);
|
||||
}
|
||||
const url = e.currentTarget.dataset.providerUrl;
|
||||
window.location.href = getConfig().LMS_BASE_URL + url;
|
||||
redirectWithDelay(getConfig().LMS_BASE_URL + url);
|
||||
}
|
||||
|
||||
const socialAuth = socialAuthProviders.map((provider, index) => (
|
||||
@@ -28,7 +47,7 @@ const SocialAuthProviders = (props) => {
|
||||
type="button"
|
||||
className={`btn-social btn-${provider.id} ${index % 2 === 0 ? 'mr-3' : ''}`}
|
||||
data-provider-url={referrer === LOGIN_PAGE ? provider.loginUrl : provider.registerUrl}
|
||||
onClick={handleSubmit}
|
||||
onClick={(event) => handleSubmit(event, provider?.name)}
|
||||
>
|
||||
{provider.iconImage ? (
|
||||
<div aria-hidden="true">
|
||||
|
||||
@@ -7,6 +7,7 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import messages from './messages';
|
||||
import { LOGIN_PAGE, REGISTER_PAGE } from '../data/constants';
|
||||
import setCookie from '../data/utils/cookies';
|
||||
|
||||
const ThirdPartyAuthAlert = (props) => {
|
||||
const { formatMessage } = useIntl();
|
||||
@@ -20,7 +21,10 @@ const ThirdPartyAuthAlert = (props) => {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,9 +4,8 @@ import { getConfig } from '@edx/frontend-platform';
|
||||
import { fetchAuthenticatedUser, getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {
|
||||
DEFAULT_REDIRECT_URL,
|
||||
} from '../data/constants';
|
||||
import { RESET_PAGE } from '../data/constants';
|
||||
import { updatePathWithQueryParams } from '../data/utils';
|
||||
|
||||
/**
|
||||
* This wrapper redirects the requester to our default redirect url if they are
|
||||
@@ -25,7 +24,12 @@ const UnAuthOnlyRoute = ({ children }) => {
|
||||
|
||||
if (isReady) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,9 +13,15 @@ export const getThirdPartyAuthContextBegin = () => ({
|
||||
type: THIRD_PARTY_AUTH_CONTEXT.BEGIN,
|
||||
});
|
||||
|
||||
export const getThirdPartyAuthContextSuccess = (fieldDescriptions, optionalFields, thirdPartyAuthContext) => ({
|
||||
export const getThirdPartyAuthContextSuccess = (
|
||||
fieldDescriptions,
|
||||
optionalFields,
|
||||
thirdPartyAuthContext,
|
||||
countriesCodesList) => ({
|
||||
type: THIRD_PARTY_AUTH_CONTEXT.SUCCESS,
|
||||
payload: { fieldDescriptions, optionalFields, thirdPartyAuthContext },
|
||||
payload: {
|
||||
fieldDescriptions, optionalFields, thirdPartyAuthContext, countriesCodesList,
|
||||
},
|
||||
});
|
||||
|
||||
export const getThirdPartyAuthContextFailure = () => ({
|
||||
|
||||
83
src/common-components/data/constants.js
Normal file
83
src/common-components/data/constants.js
Normal file
@@ -0,0 +1,83 @@
|
||||
export const registerFields = {
|
||||
fields: {
|
||||
country: {
|
||||
name: 'country',
|
||||
error_message: 'Select your country or region of residence',
|
||||
},
|
||||
honor_code: {
|
||||
name: 'honor_code',
|
||||
type: 'tos_and_honor_code',
|
||||
error_message: '',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const progressiveProfilingFields = {
|
||||
extended_profile: [],
|
||||
fields: {
|
||||
level_of_education: {
|
||||
name: 'level_of_education',
|
||||
type: 'select',
|
||||
label: 'Highest level of education completed',
|
||||
error_message: '',
|
||||
options: [
|
||||
[
|
||||
'p',
|
||||
'Doctorate',
|
||||
],
|
||||
[
|
||||
'm',
|
||||
"Master's or professional degree",
|
||||
],
|
||||
[
|
||||
'b',
|
||||
"Bachelor's degree",
|
||||
],
|
||||
[
|
||||
'a',
|
||||
'Associate degree',
|
||||
],
|
||||
[
|
||||
'hs',
|
||||
'Secondary/high school',
|
||||
],
|
||||
[
|
||||
'jhs',
|
||||
'Junior secondary/junior high/middle school',
|
||||
],
|
||||
[
|
||||
'none',
|
||||
'No formal education',
|
||||
],
|
||||
[
|
||||
'other',
|
||||
'Other education',
|
||||
],
|
||||
],
|
||||
},
|
||||
gender: {
|
||||
name: 'gender',
|
||||
type: 'select',
|
||||
label: 'Gender',
|
||||
error_message: '',
|
||||
options: [
|
||||
[
|
||||
'm',
|
||||
'Male',
|
||||
],
|
||||
[
|
||||
'f',
|
||||
'Female',
|
||||
],
|
||||
[
|
||||
'o',
|
||||
'Other/Prefer Not to Say',
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const FIELD_LABELS = {
|
||||
COUNTRY: 'country',
|
||||
};
|
||||
@@ -35,6 +35,7 @@ const reducer = (state = defaultState, action = {}) => {
|
||||
optionalFields: action.payload.optionalFields,
|
||||
thirdPartyAuthContext: action.payload.thirdPartyAuthContext,
|
||||
thirdPartyAuthApiStatus: COMPLETE_STATE,
|
||||
countriesCodesList: action.payload.countriesCodesList,
|
||||
};
|
||||
}
|
||||
case THIRD_PARTY_AUTH_CONTEXT.FAILURE:
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { logError } from '@edx/frontend-platform/logging';
|
||||
import { call, put, takeEvery } from 'redux-saga/effects';
|
||||
|
||||
@@ -7,7 +8,9 @@ import {
|
||||
getThirdPartyAuthContextSuccess,
|
||||
THIRD_PARTY_AUTH_CONTEXT,
|
||||
} from './actions';
|
||||
import { progressiveProfilingFields, registerFields } from './constants';
|
||||
import {
|
||||
getCountryList,
|
||||
getThirdPartyAuthContext,
|
||||
} from './service';
|
||||
import { setCountryFromThirdPartyAuthContext } from '../../register/data/actions';
|
||||
@@ -18,9 +21,25 @@ export function* fetchThirdPartyAuthContext(action) {
|
||||
const {
|
||||
fieldDescriptions, optionalFields, thirdPartyAuthContext,
|
||||
} = yield call(getThirdPartyAuthContext, action.payload.urlParams);
|
||||
const countriesCodesList = (yield call(getCountryList)) || [];
|
||||
|
||||
yield put(setCountryFromThirdPartyAuthContext(thirdPartyAuthContext.countryCode));
|
||||
yield put(getThirdPartyAuthContextSuccess(fieldDescriptions, optionalFields, thirdPartyAuthContext));
|
||||
// hard code country field, level of education and gender fields
|
||||
if (getConfig().ENABLE_HARD_CODE_OPTIONAL_FIELDS) {
|
||||
yield put(getThirdPartyAuthContextSuccess(
|
||||
registerFields,
|
||||
progressiveProfilingFields,
|
||||
thirdPartyAuthContext,
|
||||
countriesCodesList,
|
||||
));
|
||||
} else {
|
||||
yield put(getThirdPartyAuthContextSuccess(
|
||||
fieldDescriptions,
|
||||
optionalFields,
|
||||
thirdPartyAuthContext,
|
||||
countriesCodesList,
|
||||
));
|
||||
}
|
||||
} catch (e) {
|
||||
yield put(getThirdPartyAuthContextFailure());
|
||||
logError(e);
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { logError } from '@edx/frontend-platform/logging';
|
||||
|
||||
import { FIELD_LABELS } from './constants';
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export async function getThirdPartyAuthContext(urlParams) {
|
||||
@@ -23,3 +26,28 @@ export async function getThirdPartyAuthContext(urlParams) {
|
||||
thirdPartyAuthContext: data.contextData || {},
|
||||
};
|
||||
}
|
||||
|
||||
function extractCountryList(data) {
|
||||
return data?.fields
|
||||
.find(({ name }) => name === FIELD_LABELS.COUNTRY)
|
||||
?.options?.map(({ value }) => (value)) || [];
|
||||
}
|
||||
|
||||
export async function getCountryList() {
|
||||
try {
|
||||
const requestConfig = {
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
isPublic: true,
|
||||
};
|
||||
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.get(
|
||||
`${getConfig().LMS_BASE_URL}/user_api/v1/account/registration/`,
|
||||
requestConfig,
|
||||
);
|
||||
return extractCountryList(data);
|
||||
} catch (e) {
|
||||
logError(e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,11 @@ import * as api from '../service';
|
||||
|
||||
const { loggingService } = initializeMockLogging();
|
||||
|
||||
jest.mock('../service', () => ({
|
||||
getCountryList: jest.fn(),
|
||||
getThirdPartyAuthContext: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('fetchThirdPartyAuthContext', () => {
|
||||
const params = {
|
||||
payload: { urlParams: {} },
|
||||
@@ -31,6 +36,7 @@ describe('fetchThirdPartyAuthContext', () => {
|
||||
thirdPartyAuthContext: data,
|
||||
fieldDescriptions: {},
|
||||
optionalFields: {},
|
||||
countriesCodesList: [],
|
||||
}));
|
||||
|
||||
const dispatched = [];
|
||||
@@ -44,7 +50,7 @@ describe('fetchThirdPartyAuthContext', () => {
|
||||
expect(dispatched).toEqual([
|
||||
actions.getThirdPartyAuthContextBegin(),
|
||||
setCountryFromThirdPartyAuthContext(),
|
||||
actions.getThirdPartyAuthContextSuccess({}, {}, data),
|
||||
actions.getThirdPartyAuthContextSuccess({}, {}, data, []),
|
||||
]);
|
||||
getThirdPartyAuthContext.mockClear();
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ export { default as RedirectLogistration } from './RedirectLogistration';
|
||||
export { default as registerIcons } from './RegisterFaIcons';
|
||||
export { default as EmbeddedRegistrationRoute } from './EmbeddedRegistrationRoute';
|
||||
export { default as UnAuthOnlyRoute } from './UnAuthOnlyRoute';
|
||||
export { default as RouteTracker } from './RouteTracker';
|
||||
export { default as NotFoundPage } from './NotFoundPage';
|
||||
export { default as SocialAuthProviders } from './SocialAuthProviders';
|
||||
export { default as ThirdPartyAuthAlert } from './ThirdPartyAuthAlert';
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
@@ -37,7 +36,6 @@ describe('FormGroup', () => {
|
||||
|
||||
describe('PasswordField', () => {
|
||||
const mockStore = configureStore();
|
||||
const IntlPasswordField = injectIntl(PasswordField);
|
||||
let props = {};
|
||||
let store = {};
|
||||
|
||||
@@ -66,7 +64,7 @@ describe('PasswordField', () => {
|
||||
});
|
||||
|
||||
it('should show/hide password on icon click', () => {
|
||||
const { getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />));
|
||||
const { getByLabelText } = render(reduxWrapper(<PasswordField {...props} />));
|
||||
const passwordInput = getByLabelText('Password');
|
||||
|
||||
const showPasswordButton = getByLabelText('Show password');
|
||||
@@ -79,7 +77,7 @@ describe('PasswordField', () => {
|
||||
});
|
||||
|
||||
it('should show password requirement tooltip on focus', async () => {
|
||||
const { getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />));
|
||||
const { getByLabelText } = render(reduxWrapper(<PasswordField {...props} />));
|
||||
const passwordInput = getByLabelText('Password');
|
||||
jest.useFakeTimers();
|
||||
await act(async () => {
|
||||
@@ -96,7 +94,7 @@ describe('PasswordField', () => {
|
||||
...props,
|
||||
value: '',
|
||||
};
|
||||
const { getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />));
|
||||
const { getByLabelText } = render(reduxWrapper(<PasswordField {...props} />));
|
||||
const passwordInput = getByLabelText('Password');
|
||||
jest.useFakeTimers();
|
||||
await act(async () => {
|
||||
@@ -119,7 +117,7 @@ describe('PasswordField', () => {
|
||||
});
|
||||
|
||||
it('should update password requirement checks', async () => {
|
||||
const { getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />));
|
||||
const { getByLabelText } = render(reduxWrapper(<PasswordField {...props} />));
|
||||
const passwordInput = getByLabelText('Password');
|
||||
jest.useFakeTimers();
|
||||
await act(async () => {
|
||||
@@ -142,7 +140,7 @@ describe('PasswordField', () => {
|
||||
});
|
||||
|
||||
it('should not run validations when blur is fired on password icon click', () => {
|
||||
const { container, getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />));
|
||||
const { container, getByLabelText } = render(reduxWrapper(<PasswordField {...props} />));
|
||||
const passwordInput = container.querySelector('input[name="password"]');
|
||||
|
||||
const passwordIcon = getByLabelText('Show password');
|
||||
@@ -163,7 +161,7 @@ describe('PasswordField', () => {
|
||||
...props,
|
||||
handleBlur: jest.fn(),
|
||||
};
|
||||
const { container } = render(reduxWrapper(<IntlPasswordField {...props} />));
|
||||
const { container } = render(reduxWrapper(<PasswordField {...props} />));
|
||||
const passwordInput = container.querySelector('input[name="password"]');
|
||||
|
||||
fireEvent.blur(passwordInput, {
|
||||
@@ -181,7 +179,7 @@ describe('PasswordField', () => {
|
||||
...props,
|
||||
handleErrorChange: jest.fn(),
|
||||
};
|
||||
const { container } = render(reduxWrapper(<IntlPasswordField {...props} />));
|
||||
const { container } = render(reduxWrapper(<PasswordField {...props} />));
|
||||
const passwordInput = container.querySelector('input[name="password"]');
|
||||
|
||||
fireEvent.blur(passwordInput, {
|
||||
@@ -204,7 +202,7 @@ describe('PasswordField', () => {
|
||||
handleErrorChange: jest.fn(),
|
||||
};
|
||||
|
||||
const { getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />));
|
||||
const { getByLabelText } = render(reduxWrapper(<PasswordField {...props} />));
|
||||
|
||||
const passwordIcon = getByLabelText('Show password');
|
||||
|
||||
@@ -224,7 +222,7 @@ describe('PasswordField', () => {
|
||||
handleErrorChange: jest.fn(),
|
||||
};
|
||||
|
||||
const { getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />));
|
||||
const { getByLabelText } = render(reduxWrapper(<PasswordField {...props} />));
|
||||
|
||||
const passwordIcon = getByLabelText('Show password');
|
||||
|
||||
@@ -248,7 +246,7 @@ describe('PasswordField', () => {
|
||||
...props,
|
||||
handleErrorChange: jest.fn(),
|
||||
};
|
||||
const { getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />));
|
||||
const { getByLabelText } = render(reduxWrapper(<PasswordField {...props} />));
|
||||
const passwordField = getByLabelText('Password');
|
||||
fireEvent.blur(passwordField, {
|
||||
target: {
|
||||
@@ -268,7 +266,7 @@ describe('PasswordField', () => {
|
||||
handleErrorChange: jest.fn(),
|
||||
handleBlur: jest.fn(),
|
||||
};
|
||||
const { getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />));
|
||||
const { getByLabelText } = render(reduxWrapper(<PasswordField {...props} />));
|
||||
|
||||
const passwordIcon = getByLabelText('Show password');
|
||||
|
||||
|
||||
@@ -1,16 +1,35 @@
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import renderer from 'react-test-renderer';
|
||||
import configureStore from 'redux-mock-store';
|
||||
|
||||
import registerIcons from '../RegisterFaIcons';
|
||||
import SocialAuthProviders from '../SocialAuthProviders';
|
||||
|
||||
registerIcons();
|
||||
const mockStore = configureStore();
|
||||
|
||||
describe('SocialAuthProviders', () => {
|
||||
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 = {
|
||||
id: 'oa2-apple-id',
|
||||
name: 'Apple',
|
||||
@@ -30,11 +49,11 @@ describe('SocialAuthProviders', () => {
|
||||
it('should match social auth provider with iconImage snapshot', () => {
|
||||
props = { socialAuthProviders: [appleProvider, facebookProvider] };
|
||||
|
||||
const tree = renderer.create(
|
||||
const tree = renderer.create(reduxWrapper(
|
||||
<IntlProvider locale="en">
|
||||
<SocialAuthProviders {...props} />
|
||||
</IntlProvider>,
|
||||
).toJSON();
|
||||
)).toJSON();
|
||||
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
@@ -48,11 +67,11 @@ describe('SocialAuthProviders', () => {
|
||||
}],
|
||||
};
|
||||
|
||||
const tree = renderer.create(
|
||||
const tree = renderer.create(reduxWrapper(
|
||||
<IntlProvider locale="en">
|
||||
<SocialAuthProviders {...props} />
|
||||
</IntlProvider>,
|
||||
).toJSON();
|
||||
)).toJSON();
|
||||
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
@@ -66,11 +85,11 @@ describe('SocialAuthProviders', () => {
|
||||
}],
|
||||
};
|
||||
|
||||
const tree = renderer.create(
|
||||
const tree = renderer.create(reduxWrapper(
|
||||
<IntlProvider locale="en">
|
||||
<SocialAuthProviders {...props} />
|
||||
</IntlProvider>,
|
||||
).toJSON();
|
||||
)).toJSON();
|
||||
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ const configuration = {
|
||||
MARKETING_EMAILS_OPT_IN: process.env.MARKETING_EMAILS_OPT_IN || '',
|
||||
SHOW_CONFIGURABLE_EDX_FIELDS: process.env.SHOW_CONFIGURABLE_EDX_FIELDS || 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,
|
||||
// Links
|
||||
ACTIVATION_EMAIL_SUPPORT_LINK: process.env.ACTIVATION_EMAIL_SUPPORT_LINK || null,
|
||||
@@ -35,6 +36,8 @@ const configuration = {
|
||||
ZENDESK_LOGO_URL: process.env.ZENDESK_LOGO_URL,
|
||||
ALGOLIA_APP_ID: process.env.ALGOLIA_APP_ID || '',
|
||||
ALGOLIA_SEARCH_API_KEY: process.env.ALGOLIA_SEARCH_API_KEY || '',
|
||||
AUTO_GENERATED_USERNAME_EXPERIMENT_ID: process.env.AUTO_GENERATED_USERNAME_EXPERIMENT_ID || '',
|
||||
RECAPTCHA_SITE_KEY_WEB: process.env.RECAPTCHA_SITE_KEY_WEB || '',
|
||||
};
|
||||
|
||||
export default configuration;
|
||||
|
||||
@@ -37,3 +37,4 @@ export const VALID_EMAIL_REGEX = '(^[-!#$%&\'*+/=?^_`{}|~0-9A-Z]+(\\.[-!#$%&\'*+
|
||||
// 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 REDIRECT = 'redirect';
|
||||
export const APP_NAME = 'authn_mfe';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { combineReducers } from 'redux';
|
||||
|
||||
import { reducer as cohesionReducer, storeName as cohesionStoreName } from '../cohesion/data/reducers';
|
||||
import {
|
||||
reducer as commonComponentsReducer,
|
||||
storeName as commonComponentsStoreName,
|
||||
@@ -31,6 +32,7 @@ const createRootReducer = () => combineReducers({
|
||||
[commonComponentsStoreName]: commonComponentsReducer,
|
||||
[forgotPasswordStoreName]: forgotPasswordReducer,
|
||||
[resetPasswordStoreName]: resetPasswordReducer,
|
||||
[cohesionStoreName]: cohesionReducer,
|
||||
[authnProgressiveProfilingStoreName]: authnProgressiveProfilingReducers,
|
||||
});
|
||||
export default createRootReducer;
|
||||
|
||||
37
src/data/segment/utils.js
Normal file
37
src/data/segment/utils.js
Normal 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);
|
||||
};
|
||||
@@ -11,3 +11,11 @@ export default function setCookie(cookieName, cookieValue, cookieExpiry) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,3 +81,9 @@ export const isHostAvailableInQueryParams = () => {
|
||||
const queryParams = getAllPossibleQueryParams();
|
||||
return 'host' in queryParams;
|
||||
};
|
||||
|
||||
export const redirectWithDelay = (redirectUrl) => {
|
||||
setTimeout(() => {
|
||||
window.location.href = redirectUrl;
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
@@ -8,4 +8,4 @@ export {
|
||||
windowScrollTo,
|
||||
} from './dataUtils';
|
||||
export { default as AsyncActionType } from './reduxUtils';
|
||||
export { default as setCookie } from './cookies';
|
||||
export { default as setCookie, removeCookie } from './cookies';
|
||||
|
||||
@@ -2,7 +2,6 @@ import React, { useEffect, useState } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Form,
|
||||
@@ -25,6 +24,10 @@ import BaseContainer from '../base-container';
|
||||
import { FormGroup } from '../common-components';
|
||||
import { DEFAULT_STATE, LOGIN_PAGE, VALID_EMAIL_REGEX } from '../data/constants';
|
||||
import { updatePathWithQueryParams, windowScrollTo } from '../data/utils';
|
||||
import {
|
||||
trackForgotPasswordPageEvent,
|
||||
trackForgotPasswordPageViewed,
|
||||
} from '../tracking/trackers/forgotpassword';
|
||||
|
||||
const ForgotPasswordPage = (props) => {
|
||||
const platformName = getConfig().SITE_NAME;
|
||||
@@ -41,8 +44,8 @@ const ForgotPasswordPage = (props) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
sendPageEvent('login_and_registration', 'reset');
|
||||
sendTrackEvent('edx.bi.password_reset_form.viewed', { category: 'user-engagement' });
|
||||
trackForgotPasswordPageEvent();
|
||||
trackForgotPasswordPageViewed();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { mergeConfig } from '@edx/frontend-platform';
|
||||
import { configure, injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { configure, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
fireEvent, render, screen,
|
||||
} from '@testing-library/react';
|
||||
@@ -26,7 +25,6 @@ jest.mock('react-router-dom', () => ({
|
||||
useNavigate: () => mockedNavigator,
|
||||
}));
|
||||
|
||||
const IntlForgotPasswordPage = injectIntl(ForgotPasswordPage);
|
||||
const mockStore = configureStore();
|
||||
|
||||
const initialState = {
|
||||
@@ -78,7 +76,7 @@ describe('ForgotPasswordPage', () => {
|
||||
);
|
||||
|
||||
it('not should display need other help signing in button', () => {
|
||||
const { queryByTestId } = render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||
const { queryByTestId } = render(reduxWrapper(<ForgotPasswordPage {...props} />));
|
||||
const forgotPasswordButton = queryByTestId('forgot-password');
|
||||
expect(forgotPasswordButton).toBeNull();
|
||||
});
|
||||
@@ -87,14 +85,14 @@ describe('ForgotPasswordPage', () => {
|
||||
mergeConfig({
|
||||
LOGIN_ISSUE_SUPPORT_LINK: '/support',
|
||||
});
|
||||
render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||
render(reduxWrapper(<ForgotPasswordPage {...props} />));
|
||||
const forgotPasswordButton = screen.findByText('Need help signing in?');
|
||||
expect(forgotPasswordButton).toBeDefined();
|
||||
});
|
||||
|
||||
it('should display email validation error message', async () => {
|
||||
const validationMessage = 'We were unable to contact you.Enter a valid email address below.';
|
||||
const { container } = render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||
const { container } = render(reduxWrapper(<ForgotPasswordPage {...props} />));
|
||||
|
||||
const emailInput = screen.getByLabelText('Email');
|
||||
|
||||
@@ -115,7 +113,7 @@ describe('ForgotPasswordPage', () => {
|
||||
const expectedMessage = 'We were unable to contact you.'
|
||||
+ 'An error has occurred. Try refreshing the page, or check your internet connection.';
|
||||
|
||||
const { container } = render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||
const { container } = render(reduxWrapper(<ForgotPasswordPage {...props} />));
|
||||
|
||||
const alertElements = container.querySelectorAll('.alert-danger');
|
||||
const validationErrors = alertElements[0].textContent;
|
||||
@@ -124,7 +122,7 @@ describe('ForgotPasswordPage', () => {
|
||||
|
||||
it('should display empty email validation message', async () => {
|
||||
const validationMessage = 'We were unable to contact you.Enter your email below.';
|
||||
const { container } = render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||
const { container } = render(reduxWrapper(<ForgotPasswordPage {...props} />));
|
||||
|
||||
const submitButton = screen.getByText('Submit');
|
||||
fireEvent.click(submitButton);
|
||||
@@ -141,7 +139,7 @@ describe('ForgotPasswordPage', () => {
|
||||
forgotPassword: { status: 'forbidden' },
|
||||
});
|
||||
|
||||
const { container } = render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||
const { container } = render(reduxWrapper(<ForgotPasswordPage {...props} />));
|
||||
|
||||
const alertElements = container.querySelectorAll('.alert-danger');
|
||||
const validationErrors = alertElements[0].textContent;
|
||||
@@ -149,7 +147,7 @@ describe('ForgotPasswordPage', () => {
|
||||
});
|
||||
|
||||
it('should not display any error message on change event', () => {
|
||||
render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||
render(reduxWrapper(<ForgotPasswordPage {...props} />));
|
||||
|
||||
const emailInput = screen.getByLabelText('Email');
|
||||
|
||||
@@ -172,7 +170,7 @@ describe('ForgotPasswordPage', () => {
|
||||
};
|
||||
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||
render(reduxWrapper(<ForgotPasswordPage {...props} />));
|
||||
const emailInput = screen.getByLabelText('Email');
|
||||
|
||||
fireEvent.blur(emailInput);
|
||||
@@ -187,7 +185,7 @@ describe('ForgotPasswordPage', () => {
|
||||
emailValidationError: validationMessage,
|
||||
email: '',
|
||||
};
|
||||
const { container } = render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||
const { container } = render(reduxWrapper(<ForgotPasswordPage {...props} />));
|
||||
const validationElement = container.querySelector('.pgn__form-text-invalid');
|
||||
expect(validationElement.textContent).toEqual(validationMessage);
|
||||
});
|
||||
@@ -205,7 +203,7 @@ describe('ForgotPasswordPage', () => {
|
||||
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
|
||||
render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||
render(reduxWrapper(<ForgotPasswordPage {...props} />));
|
||||
const emailInput = screen.getByLabelText('Email');
|
||||
|
||||
fireEvent.focus(emailInput);
|
||||
@@ -219,7 +217,7 @@ describe('ForgotPasswordPage', () => {
|
||||
emailValidationError: '',
|
||||
email: '',
|
||||
};
|
||||
render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||
render(reduxWrapper(<ForgotPasswordPage {...props} />));
|
||||
const errorElement = screen.queryByTestId('email-invalid-feedback');
|
||||
expect(errorElement).toBeNull();
|
||||
});
|
||||
@@ -236,7 +234,7 @@ describe('ForgotPasswordPage', () => {
|
||||
+ 'receive a password reset message after 1 minute, verify that you entered the correct email address,'
|
||||
+ ' or check your spam folder. If you need further assistance, contact technical support.';
|
||||
|
||||
const { container } = render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||
const { container } = render(reduxWrapper(<ForgotPasswordPage {...props} />));
|
||||
const successElement = findByTextContent(container, successMessage);
|
||||
|
||||
expect(successElement).toBeDefined();
|
||||
@@ -254,7 +252,7 @@ describe('ForgotPasswordPage', () => {
|
||||
+ 'This password reset link is invalid. It may have been used already. '
|
||||
+ 'Enter your email below to receive a new link.';
|
||||
|
||||
const { container } = render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||
const { container } = render(reduxWrapper(<ForgotPasswordPage {...props} />));
|
||||
const successElement = findByTextContent(container, successMessage);
|
||||
|
||||
expect(successElement).toBeDefined();
|
||||
@@ -262,7 +260,7 @@ describe('ForgotPasswordPage', () => {
|
||||
});
|
||||
|
||||
it('should redirect onto login page', async () => {
|
||||
const { container } = render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||
const { container } = render(reduxWrapper(<ForgotPasswordPage {...props} />));
|
||||
|
||||
const navElement = container.querySelector('nav');
|
||||
const anchorElement = navElement.querySelector('a');
|
||||
|
||||
@@ -1,6 +1,2 @@
|
||||
@import "~@edx/brand/paragon/fonts";
|
||||
@import "~@edx/brand/paragon/variables";
|
||||
@import "~@openedx/paragon/scss/core/core";
|
||||
@import "~@edx/brand/paragon/overrides";
|
||||
|
||||
@use "@openedx/paragon/styles/css/core/custom-media-breakpoints" as paragonCustomMediaBreakpoints;
|
||||
@import "sass/style";
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Alert } from '@openedx/paragon';
|
||||
import { Alert, Hyperlink } from '@openedx/paragon';
|
||||
import { CheckCircle, Error } from '@openedx/paragon/icons';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
@@ -36,19 +36,17 @@ const AccountActivationMessage = ({ messageType }) => {
|
||||
break;
|
||||
}
|
||||
case ACCOUNT_ACTIVATION_MESSAGE.ERROR: {
|
||||
const supportLink = (
|
||||
<Alert.Link href={getConfig().ACTIVATION_EMAIL_SUPPORT_LINK}>
|
||||
{formatMessage(messages['account.activation.support.link'])}
|
||||
</Alert.Link>
|
||||
const supportEmail = (
|
||||
<Hyperlink isInline destination={`mailto:${getConfig().INFO_EMAIL}`}>{getConfig().INFO_EMAIL}</Hyperlink>
|
||||
);
|
||||
|
||||
heading = formatMessage(messages[`account.${activationOrConfirmation}.error.message.title`]);
|
||||
activationMessage = (
|
||||
<FormattedMessage
|
||||
id="account.activation.error.message"
|
||||
defaultMessage="Something went wrong, please {supportLink} to resolve this issue."
|
||||
defaultMessage="Something went wrong, please contact {supportEmail} to resolve this issue."
|
||||
description="Account activation error message"
|
||||
values={{ supportLink }}
|
||||
values={{ supportEmail }}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
@@ -10,19 +11,23 @@ import PropTypes from 'prop-types';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
|
||||
import messages from './messages';
|
||||
import trackCohesionEvent from '../cohesion/trackers';
|
||||
import { DEFAULT_REDIRECT_URL, RESET_PAGE } from '../data/constants';
|
||||
import { updatePathWithQueryParams } from '../data/utils';
|
||||
import { redirectWithDelay } from '../data/utils/dataUtils';
|
||||
import useMobileResponsive from '../data/utils/useMobileResponsive';
|
||||
|
||||
const ChangePasswordPrompt = ({ variant, redirectUrl }) => {
|
||||
const isMobileView = useMobileResponsive();
|
||||
const [redirectToResetPasswordPage, setRedirectToResetPasswordPage] = useState(false);
|
||||
const cohesionEventData = useSelector(state => state.cohesion.eventData);
|
||||
const handlers = {
|
||||
handleToggleOff: () => {
|
||||
if (variant === 'block') {
|
||||
setRedirectToResetPasswordPage(true);
|
||||
} else {
|
||||
window.location.href = redirectUrl || getConfig().LMS_BASE_URL.concat(DEFAULT_REDIRECT_URL);
|
||||
trackCohesionEvent(cohesionEventData);
|
||||
redirectWithDelay(redirectUrl || getConfig().LMS_BASE_URL.concat(DEFAULT_REDIRECT_URL));
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { connect, useDispatch } from 'react-redux';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { injectIntl, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Form, StatefulButton,
|
||||
} from '@openedx/paragon';
|
||||
@@ -21,6 +20,10 @@ import {
|
||||
import { INVALID_FORM, TPA_AUTHENTICATION_FAILURE } from './data/constants';
|
||||
import LoginFailureMessage from './LoginFailure';
|
||||
import messages from './messages';
|
||||
import {
|
||||
ELEMENT_NAME, ELEMENT_TEXT, ELEMENT_TYPES, PAGE_TYPES,
|
||||
} from '../cohesion/constants';
|
||||
import { setCohesionEventStates } from '../cohesion/data/actions';
|
||||
import {
|
||||
FormGroup,
|
||||
InstitutionLogistration,
|
||||
@@ -32,9 +35,7 @@ import { getThirdPartyAuthContext } from '../common-components/data/actions';
|
||||
import { thirdPartyAuthContextSelector } from '../common-components/data/selectors';
|
||||
import EnterpriseSSO from '../common-components/EnterpriseSSO';
|
||||
import ThirdPartyAuth from '../common-components/ThirdPartyAuth';
|
||||
import {
|
||||
DEFAULT_STATE, PENDING_STATE, RESET_PAGE,
|
||||
} from '../data/constants';
|
||||
import { DEFAULT_STATE, PENDING_STATE, RESET_PAGE } from '../data/constants';
|
||||
import {
|
||||
getActivationStatus,
|
||||
getAllPossibleQueryParams,
|
||||
@@ -42,7 +43,11 @@ import {
|
||||
getTpaProvider,
|
||||
updatePathWithQueryParams,
|
||||
} from '../data/utils';
|
||||
import { removeCookie } from '../data/utils/cookies';
|
||||
import ResetPasswordSuccess from '../reset-password/ResetPasswordSuccess';
|
||||
import {
|
||||
trackForgotPasswordLinkClick, trackLoginPageViewed, trackLoginSuccess,
|
||||
} from '../tracking/trackers/login';
|
||||
|
||||
const LoginPage = (props) => {
|
||||
const {
|
||||
@@ -69,6 +74,7 @@ const LoginPage = (props) => {
|
||||
getTPADataFromBackend,
|
||||
} = props;
|
||||
const { formatMessage } = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
const activationMsgType = getActivationStatus();
|
||||
const queryParams = useMemo(() => getAllPossibleQueryParams(), []);
|
||||
|
||||
@@ -78,9 +84,17 @@ const LoginPage = (props) => {
|
||||
const tpaHint = getTpaHint();
|
||||
|
||||
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(() => {
|
||||
const payload = { ...queryParams };
|
||||
if (tpaHint) {
|
||||
@@ -140,6 +154,15 @@ const LoginPage = (props) => {
|
||||
|
||||
const handleSubmit = (event) => {
|
||||
event.preventDefault();
|
||||
const eventData = {
|
||||
pageType: PAGE_TYPES.SIGN_IN,
|
||||
elementType: ELEMENT_TYPES.BUTTON,
|
||||
webElementText: ELEMENT_TEXT.SIGN_IN,
|
||||
webElementName: ELEMENT_NAME.SIGN_IN,
|
||||
};
|
||||
|
||||
dispatch(setCohesionEventStates(eventData));
|
||||
|
||||
if (showResetPasswordSuccessBanner) {
|
||||
props.dismissPasswordResetBanner();
|
||||
}
|
||||
@@ -170,9 +193,6 @@ const LoginPage = (props) => {
|
||||
const { name } = event.target;
|
||||
setErrors(prevErrors => ({ ...prevErrors, [name]: '' }));
|
||||
};
|
||||
const trackForgotPasswordLinkClick = () => {
|
||||
sendTrackEvent('edx.bi.password-reset_form.toggled', { category: 'user-engagement' });
|
||||
};
|
||||
|
||||
const { provider, skipHintedLogin } = getTpaProvider(tpaHint, providers, secondaryProviders);
|
||||
|
||||
@@ -208,6 +228,7 @@ const LoginPage = (props) => {
|
||||
success={loginResult.success}
|
||||
redirectUrl={loginResult.redirectUrl}
|
||||
finishAuthUrl={finishAuthUrl}
|
||||
currentProvider={currentProvider}
|
||||
/>
|
||||
<div className="mw-xs mt-3 mb-2">
|
||||
<LoginFailureMessage
|
||||
@@ -365,4 +386,4 @@ export default connect(
|
||||
loginRequest,
|
||||
getTPADataFromBackend: getThirdPartyAuthContext,
|
||||
},
|
||||
)(injectIntl(LoginPage));
|
||||
)(LoginPage);
|
||||
|
||||
@@ -95,11 +95,6 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Your account could not be activated',
|
||||
description: 'Account Activation error message title',
|
||||
},
|
||||
'account.activation.support.link': {
|
||||
id: 'account.activation.support.link',
|
||||
defaultMessage: 'contact support',
|
||||
description: 'Link text used in account activation error message to go to learner help center',
|
||||
},
|
||||
// Email Confirmation Strings
|
||||
'account.confirmation.success.message.title': {
|
||||
id: 'account.confirmation.success.message.title',
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import React from 'react';
|
||||
|
||||
import { mergeConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
render, screen,
|
||||
} from '@testing-library/react';
|
||||
@@ -9,8 +7,6 @@ import {
|
||||
import AccountActivationMessage from '../AccountActivationMessage';
|
||||
import { ACCOUNT_ACTIVATION_MESSAGE } from '../data/constants';
|
||||
|
||||
const IntlAccountActivationMessage = injectIntl(AccountActivationMessage);
|
||||
|
||||
describe('AccountActivationMessage', () => {
|
||||
beforeEach(() => {
|
||||
mergeConfig({
|
||||
@@ -21,7 +17,7 @@ describe('AccountActivationMessage', () => {
|
||||
it('should match account already activated message', () => {
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<IntlAccountActivationMessage messageType={ACCOUNT_ACTIVATION_MESSAGE.INFO} />
|
||||
<AccountActivationMessage messageType={ACCOUNT_ACTIVATION_MESSAGE.INFO} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
@@ -36,7 +32,7 @@ describe('AccountActivationMessage', () => {
|
||||
it('should match account activated success message', () => {
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<IntlAccountActivationMessage messageType={ACCOUNT_ACTIVATION_MESSAGE.SUCCESS} />
|
||||
<AccountActivationMessage messageType={ACCOUNT_ACTIVATION_MESSAGE.SUCCESS} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
@@ -53,12 +49,12 @@ describe('AccountActivationMessage', () => {
|
||||
it('should match account activation error message', () => {
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<IntlAccountActivationMessage messageType={ACCOUNT_ACTIVATION_MESSAGE.ERROR} />
|
||||
<AccountActivationMessage messageType={ACCOUNT_ACTIVATION_MESSAGE.ERROR} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
const expectedMessage = 'Your account could not be activated'
|
||||
+ 'Something went wrong, please contact support to resolve this issue.';
|
||||
+ 'Something went wrong, please contact to resolve this issue.';
|
||||
|
||||
expect(screen.getByText(
|
||||
'',
|
||||
@@ -69,7 +65,7 @@ describe('AccountActivationMessage', () => {
|
||||
it('should not display anything for invalid message type', () => {
|
||||
const { container } = render(
|
||||
<IntlProvider locale="en">
|
||||
<IntlAccountActivationMessage messageType="invalid-message" />
|
||||
<AccountActivationMessage messageType="invalid-message" />
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
@@ -88,7 +84,7 @@ describe('EmailConfirmationMessage', () => {
|
||||
it('should match email already confirmed message', () => {
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<IntlAccountActivationMessage messageType={ACCOUNT_ACTIVATION_MESSAGE.INFO} />
|
||||
<AccountActivationMessage messageType={ACCOUNT_ACTIVATION_MESSAGE.INFO} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
@@ -103,7 +99,7 @@ describe('EmailConfirmationMessage', () => {
|
||||
it('should match email confirmation success message', () => {
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<IntlAccountActivationMessage messageType={ACCOUNT_ACTIVATION_MESSAGE.SUCCESS} />
|
||||
<AccountActivationMessage messageType={ACCOUNT_ACTIVATION_MESSAGE.SUCCESS} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
const expectedMessage = 'Success! You have confirmed your email.Sign in to continue.';
|
||||
@@ -117,11 +113,11 @@ describe('EmailConfirmationMessage', () => {
|
||||
it('should match email confirmation error message', () => {
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<IntlAccountActivationMessage messageType={ACCOUNT_ACTIVATION_MESSAGE.ERROR} />
|
||||
<AccountActivationMessage messageType={ACCOUNT_ACTIVATION_MESSAGE.ERROR} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
const expectedMessage = 'Your email could not be confirmed'
|
||||
+ 'Something went wrong, please contact support to resolve this issue.';
|
||||
+ 'Something went wrong, please contact to resolve this issue.';
|
||||
expect(screen.getByText(
|
||||
'',
|
||||
{ selector: '#account-activation-message' },
|
||||
|
||||
@@ -1,18 +1,29 @@
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
fireEvent, render, screen,
|
||||
fireEvent, render, screen, waitFor,
|
||||
} from '@testing-library/react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import configureStore from 'redux-mock-store';
|
||||
|
||||
import mockTagular from '../../cohesion/utils';
|
||||
import { RESET_PAGE } from '../../data/constants';
|
||||
import ChangePasswordPrompt from '../ChangePasswordPrompt';
|
||||
|
||||
const IntlChangePasswordPrompt = injectIntl(ChangePasswordPrompt);
|
||||
const mockedNavigator = jest.fn();
|
||||
const mockStore = configureStore();
|
||||
mockTagular();
|
||||
|
||||
const eventData = {
|
||||
pageType: 'test-page',
|
||||
elementType: 'test-element-type',
|
||||
webElementText: 'test-element-text',
|
||||
webElementName: 'test-element-name',
|
||||
};
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...(jest.requireActual('react-router-dom')),
|
||||
@@ -21,8 +32,14 @@ jest.mock('react-router-dom', () => ({
|
||||
|
||||
describe('ChangePasswordPromptTests', () => {
|
||||
let props = {};
|
||||
let store = {};
|
||||
|
||||
const initialState = {
|
||||
cohesion: { eventData: {} },
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
store = mockStore(initialState);
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
writable: true,
|
||||
value: jest.fn().mockImplementation(query => ({
|
||||
@@ -31,38 +48,56 @@ describe('ChangePasswordPromptTests', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('[nudge modal] should redirect to next url when user clicks close button', () => {
|
||||
it('[nudge modal] should redirect to next url when user clicks close button', async () => {
|
||||
const dashboardUrl = getConfig().BASE_URL.concat('/dashboard');
|
||||
props = {
|
||||
variant: 'nudge',
|
||||
redirectUrl: dashboardUrl,
|
||||
};
|
||||
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
cohesion: {
|
||||
eventData,
|
||||
},
|
||||
});
|
||||
|
||||
delete window.location;
|
||||
window.location = { href: getConfig().BASE_URL };
|
||||
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<MemoryRouter>
|
||||
<IntlChangePasswordPrompt {...props} />
|
||||
</MemoryRouter>
|
||||
<Provider store={store}>
|
||||
<MemoryRouter>
|
||||
<ChangePasswordPrompt {...props} />
|
||||
</MemoryRouter>
|
||||
</Provider>
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText('Close'));
|
||||
expect(window.location.href).toBe(dashboardUrl);
|
||||
await waitFor(() => {
|
||||
expect(window.location.href).toBe(dashboardUrl);
|
||||
}, { timeout: 1100 });
|
||||
});
|
||||
|
||||
it('[block modal] should redirect to reset password page when user clicks outside modal', async () => {
|
||||
props = {
|
||||
variant: 'block',
|
||||
};
|
||||
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
cohesion: {
|
||||
eventData,
|
||||
},
|
||||
});
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<MemoryRouter>
|
||||
<IntlChangePasswordPrompt {...props} />
|
||||
</MemoryRouter>
|
||||
<Provider store={store}>
|
||||
<MemoryRouter>
|
||||
<ChangePasswordPrompt {...props} />
|
||||
</MemoryRouter>
|
||||
</Provider>
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
render, screen,
|
||||
} from '@testing-library/react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import configureStore from 'redux-mock-store';
|
||||
|
||||
import {
|
||||
ACCOUNT_LOCKED_OUT,
|
||||
@@ -25,13 +27,27 @@ import LoginFailureMessage from '../LoginFailure';
|
||||
jest.mock('@edx/frontend-platform/auth', () => ({
|
||||
getAuthService: jest.fn(),
|
||||
}));
|
||||
const mockStore = configureStore();
|
||||
|
||||
const IntlLoginFailureMessage = injectIntl(LoginFailureMessage);
|
||||
|
||||
const eventData = {
|
||||
pageType: 'test-page',
|
||||
elementType: 'test-element-type',
|
||||
webElementText: 'test-element-text',
|
||||
webElementName: 'test-element-name',
|
||||
};
|
||||
|
||||
describe('LoginFailureMessage', () => {
|
||||
let props = {};
|
||||
let store = {};
|
||||
|
||||
const initialState = {
|
||||
cohesion: { eventData: {} },
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
store = mockStore(initialState);
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
writable: true,
|
||||
value: jest.fn().mockImplementation(query => ({
|
||||
@@ -48,7 +64,7 @@ describe('LoginFailureMessage', () => {
|
||||
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<IntlLoginFailureMessage {...props} />
|
||||
<LoginFailureMessage {...props} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
@@ -76,7 +92,7 @@ describe('LoginFailureMessage', () => {
|
||||
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<IntlLoginFailureMessage {...props} />
|
||||
<LoginFailureMessage {...props} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
@@ -106,7 +122,7 @@ describe('LoginFailureMessage', () => {
|
||||
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<IntlLoginFailureMessage {...props} />
|
||||
<LoginFailureMessage {...props} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
@@ -132,7 +148,7 @@ describe('LoginFailureMessage', () => {
|
||||
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<IntlLoginFailureMessage {...props} />
|
||||
<LoginFailureMessage {...props} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
@@ -152,7 +168,7 @@ describe('LoginFailureMessage', () => {
|
||||
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<IntlLoginFailureMessage {...props} />
|
||||
<LoginFailureMessage {...props} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
@@ -176,7 +192,7 @@ describe('LoginFailureMessage', () => {
|
||||
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<IntlLoginFailureMessage {...props} />
|
||||
<LoginFailureMessage {...props} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
@@ -196,7 +212,7 @@ describe('LoginFailureMessage', () => {
|
||||
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<IntlLoginFailureMessage {...props} />
|
||||
<LoginFailureMessage {...props} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
@@ -216,7 +232,7 @@ describe('LoginFailureMessage', () => {
|
||||
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<IntlLoginFailureMessage {...props} />
|
||||
<LoginFailureMessage {...props} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
@@ -236,7 +252,7 @@ describe('LoginFailureMessage', () => {
|
||||
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<IntlLoginFailureMessage {...props} />
|
||||
<LoginFailureMessage {...props} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
@@ -255,7 +271,7 @@ describe('LoginFailureMessage', () => {
|
||||
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<IntlLoginFailureMessage {...props} />
|
||||
<LoginFailureMessage {...props} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
@@ -275,7 +291,7 @@ describe('LoginFailureMessage', () => {
|
||||
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<IntlLoginFailureMessage {...props} />
|
||||
<LoginFailureMessage {...props} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
@@ -298,11 +314,19 @@ describe('LoginFailureMessage', () => {
|
||||
errorCount: 0,
|
||||
};
|
||||
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
cohesion: {
|
||||
eventData,
|
||||
},
|
||||
});
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<MemoryRouter>
|
||||
<IntlLoginFailureMessage {...props} />
|
||||
</MemoryRouter>
|
||||
<Provider store={store}>
|
||||
<MemoryRouter>
|
||||
<IntlLoginFailureMessage {...props} />
|
||||
</MemoryRouter>
|
||||
</Provider>
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
@@ -323,12 +347,20 @@ describe('LoginFailureMessage', () => {
|
||||
errorCode: REQUIRE_PASSWORD_CHANGE,
|
||||
errorCount: 0,
|
||||
};
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
cohesion: {
|
||||
eventData,
|
||||
},
|
||||
});
|
||||
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<MemoryRouter>
|
||||
<IntlLoginFailureMessage {...props} />
|
||||
</MemoryRouter>
|
||||
<Provider store={store}>
|
||||
<MemoryRouter>
|
||||
<IntlLoginFailureMessage {...props} />
|
||||
</MemoryRouter>
|
||||
</Provider>
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
@@ -359,7 +391,7 @@ describe('LoginFailureMessage', () => {
|
||||
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<IntlLoginFailureMessage {...props} />
|
||||
<LoginFailureMessage {...props} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { getConfig, mergeConfig } from '@edx/frontend-platform';
|
||||
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
fireEvent, render, screen, waitFor,
|
||||
} from '@testing-library/react';
|
||||
@@ -11,7 +10,10 @@ import { act } from 'react-dom/test-utils';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import configureStore from 'redux-mock-store';
|
||||
|
||||
import { COMPLETE_STATE, LOGIN_PAGE, PENDING_STATE } from '../../data/constants';
|
||||
import mockTagular from '../../cohesion/utils';
|
||||
import {
|
||||
APP_NAME, COMPLETE_STATE, LOGIN_PAGE, PENDING_STATE,
|
||||
} from '../../data/constants';
|
||||
import { backupLoginFormBegin, dismissPasswordResetBanner, loginRequest } from '../data/actions';
|
||||
import { INTERNAL_SERVER_ERROR } from '../data/constants';
|
||||
import LoginPage from '../LoginPage';
|
||||
@@ -23,8 +25,8 @@ jest.mock('@edx/frontend-platform/analytics', () => ({
|
||||
jest.mock('@edx/frontend-platform/auth', () => ({
|
||||
getAuthService: jest.fn(),
|
||||
}));
|
||||
mockTagular();
|
||||
|
||||
const IntlLoginPage = injectIntl(LoginPage);
|
||||
const mockStore = configureStore();
|
||||
|
||||
describe('LoginPage', () => {
|
||||
@@ -56,6 +58,7 @@ describe('LoginPage', () => {
|
||||
register: {
|
||||
validationApiRateLimited: false,
|
||||
},
|
||||
cohesion: { eventData: {} },
|
||||
};
|
||||
|
||||
const secondaryProviders = {
|
||||
@@ -88,7 +91,7 @@ describe('LoginPage', () => {
|
||||
it('should submit form for valid input', () => {
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
|
||||
render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
render(reduxWrapper(<LoginPage {...props} />));
|
||||
|
||||
fireEvent.change(screen.getByText(
|
||||
'',
|
||||
@@ -109,7 +112,7 @@ describe('LoginPage', () => {
|
||||
|
||||
it('should not dispatch loginRequest on empty form submission', () => {
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
render(reduxWrapper(<LoginPage {...props} />));
|
||||
|
||||
fireEvent.click(screen.getByText(
|
||||
'',
|
||||
@@ -128,7 +131,7 @@ describe('LoginPage', () => {
|
||||
});
|
||||
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
render(reduxWrapper(<LoginPage {...props} />));
|
||||
fireEvent.click(screen.getByText(
|
||||
'',
|
||||
{ selector: '.btn-brand' },
|
||||
@@ -142,7 +145,7 @@ describe('LoginPage', () => {
|
||||
it('should match state for invalid email (less than 2 characters), on form submission', () => {
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
|
||||
render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
render(reduxWrapper(<LoginPage {...props} />));
|
||||
|
||||
fireEvent.change(screen.getByText(
|
||||
'',
|
||||
@@ -162,7 +165,7 @@ describe('LoginPage', () => {
|
||||
});
|
||||
|
||||
it('should show error messages for required fields on empty form submission', () => {
|
||||
const { container } = render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
const { container } = render(reduxWrapper(<LoginPage {...props} />));
|
||||
fireEvent.click(screen.getByText(
|
||||
'',
|
||||
{ selector: '.btn-brand' },
|
||||
@@ -176,7 +179,7 @@ describe('LoginPage', () => {
|
||||
});
|
||||
|
||||
it('should run frontend validations for emailOrUsername field on form submission', () => {
|
||||
const { container } = render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
const { container } = render(reduxWrapper(<LoginPage {...props} />));
|
||||
|
||||
fireEvent.change(screen.getByText(
|
||||
'',
|
||||
@@ -195,7 +198,7 @@ describe('LoginPage', () => {
|
||||
it('should reset field related error messages on onFocus event', async () => {
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
|
||||
render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
render(reduxWrapper(<LoginPage {...props} />));
|
||||
|
||||
await act(async () => {
|
||||
// clicking submit button with empty fields to make the errors appear
|
||||
@@ -224,7 +227,7 @@ describe('LoginPage', () => {
|
||||
// ******** test form buttons and links ********
|
||||
|
||||
it('should match default button state', () => {
|
||||
render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
render(reduxWrapper(<LoginPage {...props} />));
|
||||
expect(screen.getByText('Sign in')).toBeDefined();
|
||||
});
|
||||
|
||||
@@ -237,7 +240,7 @@ describe('LoginPage', () => {
|
||||
},
|
||||
});
|
||||
|
||||
render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
render(reduxWrapper(<LoginPage {...props} />));
|
||||
|
||||
expect(screen.getByText(
|
||||
'pending',
|
||||
@@ -245,7 +248,7 @@ describe('LoginPage', () => {
|
||||
});
|
||||
|
||||
it('should show forgot password link', () => {
|
||||
render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
render(reduxWrapper(<LoginPage {...props} />));
|
||||
|
||||
expect(screen.getByText(
|
||||
'Forgot password',
|
||||
@@ -265,7 +268,7 @@ describe('LoginPage', () => {
|
||||
},
|
||||
});
|
||||
|
||||
render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
render(reduxWrapper(<LoginPage {...props} />));
|
||||
expect(screen.getByText(
|
||||
'',
|
||||
{ selector: `#${ssoProvider.id}` },
|
||||
@@ -287,7 +290,7 @@ describe('LoginPage', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const { queryByText } = render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
const { queryByText } = render(reduxWrapper(<LoginPage {...props} />));
|
||||
expect(queryByText('Company or school credentials')).toBeNull();
|
||||
expect(queryByText('Or sign in with:')).toBeNull();
|
||||
expect(queryByText('Institution/campus credentials')).toBeNull();
|
||||
@@ -307,7 +310,7 @@ describe('LoginPage', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const { queryByText } = render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
const { queryByText } = render(reduxWrapper(<LoginPage {...props} />));
|
||||
expect(queryByText('Company or school credentials')).toBeNull();
|
||||
expect(queryByText('Or sign in with:')).toBeNull();
|
||||
});
|
||||
@@ -327,7 +330,7 @@ describe('LoginPage', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const { queryByText } = render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
const { queryByText } = render(reduxWrapper(<LoginPage {...props} />));
|
||||
expect(queryByText('Or sign in with:')).toBeDefined();
|
||||
expect(queryByText('Company or school credentials')).toBeDefined();
|
||||
expect(queryByText('Institution/campus credentials')).toBeDefined();
|
||||
@@ -352,7 +355,7 @@ describe('LoginPage', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const { queryByText } = render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
const { queryByText } = render(reduxWrapper(<LoginPage {...props} />));
|
||||
expect(queryByText('Or sign in with:')).toBeDefined();
|
||||
expect(queryByText('Company or school credentials')).toBeNull();
|
||||
expect(queryByText('Institution/campus credentials')).toBeDefined();
|
||||
@@ -380,7 +383,7 @@ describe('LoginPage', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const { queryByText } = render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
const { queryByText } = render(reduxWrapper(<LoginPage {...props} />));
|
||||
expect(queryByText('Or sign in with:')).toBeDefined();
|
||||
expect(queryByText('Institution/campus credentials')).toBeDefined();
|
||||
|
||||
@@ -400,7 +403,7 @@ describe('LoginPage', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const { queryByText } = render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
const { queryByText } = render(reduxWrapper(<LoginPage {...props} />));
|
||||
expect(queryByText('Or sign in with:')).toBeNull();
|
||||
expect(queryByText('Institution/campus credentials')).toBeNull();
|
||||
expect(queryByText('Company or school credentials')).toBeNull();
|
||||
@@ -418,7 +421,7 @@ describe('LoginPage', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const { queryByText } = render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
const { queryByText } = render(reduxWrapper(<LoginPage {...props} />));
|
||||
expect(queryByText('Or sign in with:')).toBeDefined();
|
||||
expect(queryByText('Company or school credentials')).toBeNull();
|
||||
expect(queryByText('Institution/campus credentials')).toBeDefined();
|
||||
@@ -441,7 +444,7 @@ describe('LoginPage', () => {
|
||||
},
|
||||
});
|
||||
|
||||
render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
render(reduxWrapper(<LoginPage {...props} />));
|
||||
expect(screen.getByText(
|
||||
'',
|
||||
{ selector: '#login-failure-alert' },
|
||||
@@ -465,7 +468,7 @@ describe('LoginPage', () => {
|
||||
+ 'linked '}${ getConfig().SITE_NAME } account. To link your accounts, sign in now using your ${
|
||||
getConfig().SITE_NAME } password.`;
|
||||
|
||||
render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
render(reduxWrapper(<LoginPage {...props} />));
|
||||
expect(screen.getByText(
|
||||
'',
|
||||
{ selector: '#tpa-alert' },
|
||||
@@ -484,7 +487,7 @@ describe('LoginPage', () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
render(reduxWrapper(<LoginPage {...props} />));
|
||||
expect(screen.getByText(
|
||||
'',
|
||||
{ selector: '#login-failure-alert' },
|
||||
@@ -501,7 +504,7 @@ describe('LoginPage', () => {
|
||||
},
|
||||
});
|
||||
|
||||
render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
render(reduxWrapper(<LoginPage {...props} />));
|
||||
expect(screen.getByText(
|
||||
'',
|
||||
{ selector: '#login-failure-alert' },
|
||||
@@ -510,7 +513,7 @@ describe('LoginPage', () => {
|
||||
|
||||
// ******** test redirection ********
|
||||
|
||||
it('should redirect to url returned by login endpoint after successful authentication', () => {
|
||||
it('should redirect to url returned by login endpoint after successful authentication', async () => {
|
||||
const dashboardURL = 'https://test.com/testing-dashboard/';
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
@@ -525,11 +528,13 @@ describe('LoginPage', () => {
|
||||
|
||||
delete window.location;
|
||||
window.location = { href: getConfig().BASE_URL };
|
||||
render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(window.location.href).toBe(dashboardURL);
|
||||
render(reduxWrapper(<LoginPage {...props} />));
|
||||
await waitFor(() => {
|
||||
expect(window.location.href).toBe(dashboardURL);
|
||||
}, { timeout: 1100 });
|
||||
});
|
||||
|
||||
it('should redirect to finishAuthUrl upon successful login via SSO', () => {
|
||||
it('should redirect to finishAuthUrl upon successful login via SSO', async () => {
|
||||
const authCompleteUrl = '/auth/complete/google-oauth2/';
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
@@ -552,11 +557,13 @@ describe('LoginPage', () => {
|
||||
delete window.location;
|
||||
window.location = { href: getConfig().BASE_URL };
|
||||
|
||||
render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + authCompleteUrl);
|
||||
render(reduxWrapper(<LoginPage {...props} />));
|
||||
await waitFor(() => {
|
||||
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + authCompleteUrl);
|
||||
}, { timeout: 1100 });
|
||||
});
|
||||
|
||||
it('should redirect to social auth provider url on SSO button click', () => {
|
||||
it('should redirect to social auth provider url on SSO button click', async () => {
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
commonComponents: {
|
||||
@@ -571,16 +578,18 @@ describe('LoginPage', () => {
|
||||
delete window.location;
|
||||
window.location = { href: getConfig().BASE_URL };
|
||||
|
||||
render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
render(reduxWrapper(<LoginPage {...props} />));
|
||||
|
||||
fireEvent.click(screen.getByText(
|
||||
'',
|
||||
{ selector: '#oa2-apple-id' },
|
||||
));
|
||||
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + ssoProvider.loginUrl);
|
||||
await waitFor(() => {
|
||||
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + ssoProvider.loginUrl);
|
||||
}, { timeout: 1100 });
|
||||
});
|
||||
|
||||
it('should redirect to finishAuthUrl upon successful authentication via SSO', () => {
|
||||
it('should redirect to finishAuthUrl upon successful authentication via SSO', async () => {
|
||||
const finishAuthUrl = '/auth/complete/google-oauth2/';
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
@@ -600,8 +609,10 @@ describe('LoginPage', () => {
|
||||
delete window.location;
|
||||
window.location = { href: getConfig().BASE_URL };
|
||||
|
||||
render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + finishAuthUrl);
|
||||
render(reduxWrapper(<LoginPage {...props} />));
|
||||
await waitFor(() => {
|
||||
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + finishAuthUrl);
|
||||
}, { timeout: 1100 });
|
||||
});
|
||||
|
||||
// ******** test hinted third party auth ********
|
||||
@@ -622,7 +633,7 @@ describe('LoginPage', () => {
|
||||
delete window.location;
|
||||
window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: `?next=/dashboard&tpa_hint=${ssoProvider.id}` };
|
||||
|
||||
render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
render(reduxWrapper(<LoginPage {...props} />));
|
||||
expect(screen.getByText(
|
||||
'',
|
||||
{ selector: `#${ssoProvider.id}` },
|
||||
@@ -649,7 +660,7 @@ describe('LoginPage', () => {
|
||||
delete window.location;
|
||||
window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: `?next=/dashboard&tpa_hint=${ssoProvider.id}` };
|
||||
|
||||
const { container } = render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
const { container } = render(reduxWrapper(<LoginPage {...props} />));
|
||||
expect(container.querySelector('.react-loading-skeleton')).toBeTruthy();
|
||||
});
|
||||
|
||||
@@ -671,7 +682,7 @@ describe('LoginPage', () => {
|
||||
window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: `?next=/dashboard&tpa_hint=${secondaryProviders.id}` };
|
||||
secondaryProviders.iconImage = null;
|
||||
|
||||
render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
render(reduxWrapper(<LoginPage {...props} />));
|
||||
expect(window.location.href).toEqual(getConfig().LMS_BASE_URL + secondaryProviders.loginUrl);
|
||||
});
|
||||
|
||||
@@ -691,7 +702,7 @@ describe('LoginPage', () => {
|
||||
delete window.location;
|
||||
window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: '?next=/dashboard&tpa_hint=invalid' };
|
||||
|
||||
const { container } = render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
const { container } = render(reduxWrapper(<LoginPage {...props} />));
|
||||
expect(container.querySelector(`#${ssoProvider.id}`).querySelector('#provider-name').textContent).toEqual(`${ssoProvider.name}`);
|
||||
|
||||
mergeConfig({
|
||||
@@ -715,7 +726,7 @@ describe('LoginPage', () => {
|
||||
delete window.location;
|
||||
window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: `?tpa_hint=${ssoProvider.id}` };
|
||||
|
||||
render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
render(reduxWrapper(<LoginPage {...props} />));
|
||||
expect(screen.getByText(
|
||||
'Show me other ways to sign in or register',
|
||||
).textContent).toBeDefined();
|
||||
@@ -741,7 +752,7 @@ describe('LoginPage', () => {
|
||||
delete window.location;
|
||||
window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: `?tpa_hint=${ssoProvider.id}` };
|
||||
|
||||
render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
render(reduxWrapper(<LoginPage {...props} />));
|
||||
expect(screen.getByText(
|
||||
'Show me other ways to sign in',
|
||||
).textContent).toBeDefined();
|
||||
@@ -750,8 +761,8 @@ describe('LoginPage', () => {
|
||||
// ******** miscellaneous tests ********
|
||||
|
||||
it('should send page event when login page is rendered', () => {
|
||||
render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'login');
|
||||
render(reduxWrapper(<LoginPage {...props} />));
|
||||
expect(sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'login', { app_name: APP_NAME });
|
||||
});
|
||||
|
||||
it('tests that form is in invalid state when it is submitted', () => {
|
||||
@@ -764,7 +775,7 @@ describe('LoginPage', () => {
|
||||
});
|
||||
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
render(reduxWrapper(<LoginPage {...props} />));
|
||||
expect(store.dispatch).toHaveBeenCalledWith(backupLoginFormBegin(
|
||||
{
|
||||
formFields: {
|
||||
@@ -778,13 +789,13 @@ describe('LoginPage', () => {
|
||||
});
|
||||
|
||||
it('should send track event when forgot password link is clicked', () => {
|
||||
render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
render(reduxWrapper(<LoginPage {...props} />));
|
||||
fireEvent.click(screen.getByText(
|
||||
'Forgot password',
|
||||
{ selector: '#forgot-password' },
|
||||
));
|
||||
|
||||
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', () => {
|
||||
@@ -797,7 +808,7 @@ describe('LoginPage', () => {
|
||||
});
|
||||
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
render(reduxWrapper(<LoginPage {...props} />));
|
||||
expect(store.dispatch).toHaveBeenCalledWith(backupLoginFormBegin(
|
||||
{
|
||||
formFields: {
|
||||
@@ -826,7 +837,7 @@ describe('LoginPage', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const { container } = render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
const { container } = render(reduxWrapper(<LoginPage {...props} />));
|
||||
expect(container.querySelector('input#emailOrUsername').value).toEqual('john_doe');
|
||||
expect(container.querySelector('input#password').value).toEqual('test-password');
|
||||
});
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
tpaProvidersSelector,
|
||||
} from '../common-components/data/selectors';
|
||||
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 {
|
||||
getTpaHint, getTpaProvider, updatePathWithQueryParams,
|
||||
} from '../data/utils';
|
||||
@@ -56,11 +56,11 @@ const Logistration = (props) => {
|
||||
}, [navigate, disablePublicAccountCreation]);
|
||||
|
||||
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') {
|
||||
sendPageEvent('login_and_registration', e === '/login' ? 'login' : 'register');
|
||||
sendPageEvent('login_and_registration', e === '/login' ? 'login' : 'register', { app_name: APP_NAME });
|
||||
} else {
|
||||
sendPageEvent('login_and_registration', e.target.dataset.eventName);
|
||||
sendPageEvent('login_and_registration', e.target.dataset.eventName, { app_name: APP_NAME });
|
||||
}
|
||||
|
||||
setInstitutionLogin(!institutionLogin);
|
||||
@@ -70,7 +70,8 @@ const Logistration = (props) => {
|
||||
if (tabKey === currentTab) {
|
||||
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();
|
||||
if (tabKey === LOGIN_PAGE) {
|
||||
props.backupRegistrationForm();
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { getConfig, mergeConfig } from '@edx/frontend-platform';
|
||||
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { configure, injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { configure, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import configureStore from 'redux-mock-store';
|
||||
@@ -11,19 +10,23 @@ import configureStore from 'redux-mock-store';
|
||||
import Logistration from './Logistration';
|
||||
import { clearThirdPartyAuthContextErrorMessage } from '../common-components/data/actions';
|
||||
import {
|
||||
APP_NAME,
|
||||
COMPLETE_STATE, LOGIN_PAGE, REGISTER_PAGE,
|
||||
} from '../data/constants';
|
||||
import { backupLoginForm } from '../login/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', () => ({
|
||||
sendPageEvent: jest.fn(),
|
||||
sendTrackEvent: jest.fn(),
|
||||
}));
|
||||
jest.mock('@edx/frontend-platform/auth');
|
||||
jest.mock('../register/data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation', () => jest.fn());
|
||||
|
||||
const mockStore = configureStore();
|
||||
const IntlLogistration = injectIntl(Logistration);
|
||||
|
||||
describe('Logistration', () => {
|
||||
let store = {};
|
||||
@@ -64,6 +67,7 @@ describe('Logistration', () => {
|
||||
usernameSuggestions: [],
|
||||
validationApiRateLimited: false,
|
||||
},
|
||||
cohesion: { eventData: {} },
|
||||
commonComponents: {
|
||||
thirdPartyAuthContext: {
|
||||
providers: [],
|
||||
@@ -84,6 +88,7 @@ describe('Logistration', () => {
|
||||
})),
|
||||
}));
|
||||
|
||||
useAutoGeneratedUsernameExperimentVariation.mockReturnValue(NOT_INITIALIZED);
|
||||
configure({
|
||||
loggingService: { logError: jest.fn() },
|
||||
config: {
|
||||
@@ -95,7 +100,7 @@ describe('Logistration', () => {
|
||||
});
|
||||
|
||||
it('should do nothing when user clicks on the same tab (login/register) again', () => {
|
||||
const { container } = render(reduxWrapper(<IntlLogistration />));
|
||||
const { container } = render(reduxWrapper(<Logistration />));
|
||||
// While staying on the registration form, clicking the register tab again
|
||||
fireEvent.click(container.querySelector('a[data-rb-event-key="/register"]'));
|
||||
|
||||
@@ -107,14 +112,14 @@ describe('Logistration', () => {
|
||||
ALLOW_PUBLIC_ACCOUNT_CREATION: true,
|
||||
});
|
||||
|
||||
const { container } = render(reduxWrapper(<IntlLogistration />));
|
||||
const { container } = render(reduxWrapper(<Logistration />));
|
||||
|
||||
expect(container.querySelector('RegistrationPage')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should render login page', () => {
|
||||
const props = { selectedPage: LOGIN_PAGE };
|
||||
const { container } = render(reduxWrapper(<IntlLogistration {...props} />));
|
||||
const { container } = render(reduxWrapper(<Logistration {...props} />));
|
||||
|
||||
expect(container.querySelector('LoginPage')).toBeDefined();
|
||||
});
|
||||
@@ -125,7 +130,7 @@ describe('Logistration', () => {
|
||||
});
|
||||
|
||||
let props = { selectedPage: LOGIN_PAGE };
|
||||
const { rerender } = render(reduxWrapper(<IntlLogistration {...props} />));
|
||||
const { rerender } = render(reduxWrapper(<Logistration {...props} />));
|
||||
|
||||
// verifying sign in heading
|
||||
expect(screen.getByRole('heading', { level: 3 }).textContent).toEqual('Sign in');
|
||||
@@ -133,7 +138,7 @@ describe('Logistration', () => {
|
||||
// register page is still accessible when SHOW_REGISTRATION_LINKS is false
|
||||
// but it needs to be accessed directly
|
||||
props = { selectedPage: REGISTER_PAGE };
|
||||
rerender(reduxWrapper(<IntlLogistration {...props} />));
|
||||
rerender(reduxWrapper(<Logistration {...props} />));
|
||||
|
||||
// verifying register heading
|
||||
expect(screen.getByRole('heading', { level: 3 }).textContent).toEqual('Register');
|
||||
@@ -160,7 +165,7 @@ describe('Logistration', () => {
|
||||
});
|
||||
|
||||
const props = { selectedPage: LOGIN_PAGE };
|
||||
const { container } = render(reduxWrapper(<IntlLogistration {...props} />));
|
||||
const { container } = render(reduxWrapper(<Logistration {...props} />));
|
||||
|
||||
// verifying sign in heading for institution login false
|
||||
expect(screen.getByRole('heading', { level: 3 }).textContent).toEqual('Sign in');
|
||||
@@ -190,7 +195,7 @@ describe('Logistration', () => {
|
||||
});
|
||||
|
||||
const props = { selectedPage: LOGIN_PAGE };
|
||||
render(reduxWrapper(<IntlLogistration {...props} />));
|
||||
render(reduxWrapper(<Logistration {...props} />));
|
||||
expect(screen.getByText('Institution/campus credentials')).toBeDefined();
|
||||
|
||||
// on clicking "Institution/campus credentials" button, it should display institution login page
|
||||
@@ -221,11 +226,11 @@ describe('Logistration', () => {
|
||||
});
|
||||
|
||||
const props = { selectedPage: LOGIN_PAGE };
|
||||
render(reduxWrapper(<IntlLogistration {...props} />));
|
||||
render(reduxWrapper(<Logistration {...props} />));
|
||||
fireEvent.click(screen.getByText('Institution/campus credentials'));
|
||||
|
||||
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.institution_login_form.toggled', { category: 'user-engagement' });
|
||||
expect(sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'institution_login');
|
||||
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.institution_login_form.toggled', { category: 'user-engagement', app_name: APP_NAME });
|
||||
expect(sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'institution_login', { app_name: APP_NAME });
|
||||
|
||||
mergeConfig({
|
||||
DISABLE_ENTERPRISE_LOGIN: '',
|
||||
@@ -253,7 +258,7 @@ describe('Logistration', () => {
|
||||
delete window.location;
|
||||
window.location = { hostname: getConfig().SITE_NAME, href: getConfig().BASE_URL };
|
||||
|
||||
render(reduxWrapper(<IntlLogistration />));
|
||||
render(reduxWrapper(<Logistration />));
|
||||
fireEvent.click(screen.getByText('Institution/campus credentials'));
|
||||
expect(screen.getByText('Test University')).toBeDefined();
|
||||
|
||||
@@ -264,7 +269,7 @@ describe('Logistration', () => {
|
||||
|
||||
it('should fire action to backup registration form on tab click', () => {
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
const { container } = render(reduxWrapper(<IntlLogistration />));
|
||||
const { container } = render(reduxWrapper(<Logistration />));
|
||||
fireEvent.click(container.querySelector('a[data-rb-event-key="/login"]'));
|
||||
expect(store.dispatch).toHaveBeenCalledWith(backupRegistrationForm());
|
||||
});
|
||||
@@ -272,14 +277,14 @@ describe('Logistration', () => {
|
||||
it('should fire action to backup login form on tab click', () => {
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
const props = { selectedPage: LOGIN_PAGE };
|
||||
const { container } = render(reduxWrapper(<IntlLogistration {...props} />));
|
||||
const { container } = render(reduxWrapper(<Logistration {...props} />));
|
||||
fireEvent.click(container.querySelector('a[data-rb-event-key="/register"]'));
|
||||
expect(store.dispatch).toHaveBeenCalledWith(backupLoginForm());
|
||||
});
|
||||
|
||||
it('should clear tpa context errorMessage tab click', () => {
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
const { container } = render(reduxWrapper(<IntlLogistration />));
|
||||
const { container } = render(reduxWrapper(<Logistration />));
|
||||
fireEvent.click(container.querySelector('a[data-rb-event-key="/login"]'));
|
||||
expect(store.dispatch).toHaveBeenCalledWith(clearThirdPartyAuthContextErrorMessage());
|
||||
});
|
||||
|
||||
29
src/plugin-slots/MainAppSlot/MainAppSlot.test.jsx
Normal file
29
src/plugin-slots/MainAppSlot/MainAppSlot.test.jsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { PluginSlot } from '@openedx/frontend-plugin-framework';
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
import MainAppSlot from './index';
|
||||
|
||||
jest.mock('@openedx/frontend-plugin-framework', () => ({
|
||||
PluginSlot: jest.fn(() => null),
|
||||
}));
|
||||
|
||||
describe('MainAppSlot', () => {
|
||||
it('renders without crashing', () => {
|
||||
render(<MainAppSlot />);
|
||||
});
|
||||
|
||||
it('renders a PluginSlot component', () => {
|
||||
render(<MainAppSlot />);
|
||||
expect(PluginSlot).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('passes the correct id prop to PluginSlot', () => {
|
||||
render(<MainAppSlot />);
|
||||
expect(PluginSlot).toHaveBeenCalledWith({ id: 'main_app_slot' }, {});
|
||||
});
|
||||
|
||||
it('does not render any children', () => {
|
||||
const { container } = render(<MainAppSlot />);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
});
|
||||
41
src/plugin-slots/MainAppSlot/README.md
Normal file
41
src/plugin-slots/MainAppSlot/README.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Main App Slot
|
||||
|
||||
### Slot ID: `main_app_slot`
|
||||
|
||||
## Description
|
||||
|
||||
This slot is used for adding content at the root level.
|
||||
|
||||
## Example
|
||||
|
||||
The following `env.config.jsx` will render a component at the MFE root level.
|
||||
|
||||

|
||||
|
||||
```js
|
||||
import {
|
||||
DIRECT_PLUGIN,
|
||||
PLUGIN_OPERATIONS,
|
||||
} from "@openedx/frontend-plugin-framework";
|
||||
import { ExampleComponent } from "@openedx/frontend-plugin-example";
|
||||
|
||||
const config = {
|
||||
pluginSlots: {
|
||||
main_app_slot: {
|
||||
plugins: [
|
||||
{
|
||||
op: PLUGIN_OPERATIONS.Insert,
|
||||
widget: {
|
||||
id: "example-component",
|
||||
type: DIRECT_PLUGIN,
|
||||
priority: 60,
|
||||
RenderWidget: ExampleComponent,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
```
|
||||
BIN
src/plugin-slots/MainAppSlot/images/main_app_slot.png
Normal file
BIN
src/plugin-slots/MainAppSlot/images/main_app_slot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 269 KiB |
7
src/plugin-slots/MainAppSlot/index.jsx
Normal file
7
src/plugin-slots/MainAppSlot/index.jsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import { PluginSlot } from '@openedx/frontend-plugin-framework';
|
||||
|
||||
const MainAppSlot = () => (
|
||||
<PluginSlot id="main_app_slot" />
|
||||
);
|
||||
|
||||
export default MainAppSlot;
|
||||
3
src/plugin-slots/README.md
Normal file
3
src/plugin-slots/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# `frontend-app-authn` Plugin Slots
|
||||
|
||||
- [`main_app_slot`](./MainAppSlot/)
|
||||
@@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { getConfig, snakeCaseObject } from '@edx/frontend-platform';
|
||||
import { identifyAuthenticatedUser, sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { identifyAuthenticatedUser } from '@edx/frontend-platform/analytics';
|
||||
import {
|
||||
AxiosJwtAuthService,
|
||||
configure as configureAuth,
|
||||
@@ -39,6 +39,13 @@ import {
|
||||
import isOneTrustFunctionalCookieEnabled from '../data/oneTrust';
|
||||
import { getAllPossibleQueryParams, isHostAvailableInQueryParams } from '../data/utils';
|
||||
import { FormFieldRenderer } from '../field-renderer';
|
||||
import {
|
||||
trackDisablePostRegistrationRecommendations,
|
||||
trackProgressiveProfilingPageViewed,
|
||||
trackProgressiveProfilingSkipLinkClick,
|
||||
trackProgressiveProfilingSubmitClick,
|
||||
trackProgressiveProfilingSupportLinkCLick,
|
||||
} from '../tracking/trackers/progressive-profiling';
|
||||
|
||||
const ProgressiveProfiling = (props) => {
|
||||
const { formatMessage } = useIntl();
|
||||
@@ -98,14 +105,13 @@ const ProgressiveProfiling = (props) => {
|
||||
useEffect(() => {
|
||||
if (authenticatedUser?.userId) {
|
||||
identifyAuthenticatedUser(authenticatedUser.userId);
|
||||
sendPageEvent('login_and_registration', 'welcome');
|
||||
trackProgressiveProfilingPageViewed();
|
||||
}
|
||||
}, [authenticatedUser]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enablePostRegistrationRecommendations) {
|
||||
sendTrackEvent(
|
||||
'edx.bi.user.recommendations.not.enabled',
|
||||
trackDisablePostRegistrationRecommendations(
|
||||
{ functionalCookiesConsent, page: 'authn_recommendations' },
|
||||
);
|
||||
return;
|
||||
@@ -149,29 +155,23 @@ const ProgressiveProfiling = (props) => {
|
||||
});
|
||||
}
|
||||
props.saveUserProfile(authenticatedUser.username, snakeCaseObject(payload));
|
||||
|
||||
sendTrackEvent(
|
||||
'edx.bi.welcome.page.submit.clicked',
|
||||
{
|
||||
isGenderSelected: !!values.gender,
|
||||
isYearOfBirthSelected: !!values.year_of_birth,
|
||||
isLevelOfEducationSelected: !!values.level_of_education,
|
||||
isWorkExperienceSelected: !!values.work_experience,
|
||||
host: queryParams?.host || '',
|
||||
},
|
||||
);
|
||||
const eventProperties = {
|
||||
isGenderSelected: !!values.gender,
|
||||
isYearOfBirthSelected: !!values.year_of_birth,
|
||||
isLevelOfEducationSelected: !!values.level_of_education,
|
||||
isWorkExperienceSelected: !!values.work_experience,
|
||||
host: queryParams?.host || '',
|
||||
};
|
||||
trackProgressiveProfilingSubmitClick(eventProperties);
|
||||
};
|
||||
|
||||
const handleSkip = (e) => {
|
||||
e.preventDefault();
|
||||
window.history.replaceState(location.state, null, '');
|
||||
setShowModal(true);
|
||||
sendTrackEvent(
|
||||
'edx.bi.welcome.page.skip.link.clicked',
|
||||
{
|
||||
host: queryParams?.host || '',
|
||||
},
|
||||
);
|
||||
trackProgressiveProfilingSkipLinkClick({
|
||||
host: queryParams?.host || '',
|
||||
});
|
||||
};
|
||||
|
||||
const onChangeHandler = (e) => {
|
||||
@@ -242,7 +242,7 @@ const ProgressiveProfiling = (props) => {
|
||||
destination={getConfig().AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK}
|
||||
target="_blank"
|
||||
showLaunchIcon={false}
|
||||
onClick={() => (sendTrackEvent('edx.bi.welcome.page.support.link.clicked'))}
|
||||
onClick={() => (trackProgressiveProfilingSupportLinkCLick())}
|
||||
>
|
||||
{formatMessage(messages['optional.fields.information.link'])}
|
||||
</Hyperlink>
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { getConfig, mergeConfig } from '@edx/frontend-platform';
|
||||
import { identifyAuthenticatedUser, sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import { configure, injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { configure, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
fireEvent, render, screen,
|
||||
fireEvent, render, screen, waitFor,
|
||||
} from '@testing-library/react';
|
||||
import { MemoryRouter, mockNavigate, useLocation } from 'react-router-dom';
|
||||
import configureStore from 'redux-mock-store';
|
||||
|
||||
import mockTagular from '../../cohesion/utils';
|
||||
import {
|
||||
APP_NAME,
|
||||
AUTHN_PROGRESSIVE_PROFILING,
|
||||
COMPLETE_STATE, DEFAULT_REDIRECT_URL,
|
||||
EMBEDDED,
|
||||
@@ -22,8 +23,8 @@ import {
|
||||
import { saveUserProfile } from '../data/actions';
|
||||
import ProgressiveProfiling from '../ProgressiveProfiling';
|
||||
|
||||
const IntlProgressiveProfilingPage = injectIntl(ProgressiveProfiling);
|
||||
const mockStore = configureStore();
|
||||
mockTagular();
|
||||
|
||||
jest.mock('@edx/frontend-platform/analytics', () => ({
|
||||
sendPageEvent: jest.fn(),
|
||||
@@ -54,6 +55,13 @@ jest.mock('react-router-dom', () => {
|
||||
};
|
||||
});
|
||||
|
||||
const eventData = {
|
||||
pageType: 'test-page',
|
||||
elementType: 'test-element-type',
|
||||
webElementText: 'test-element-text',
|
||||
webElementName: 'test-element-name',
|
||||
};
|
||||
|
||||
describe('ProgressiveProfilingTests', () => {
|
||||
let store = {};
|
||||
|
||||
@@ -114,7 +122,7 @@ describe('ProgressiveProfilingTests', () => {
|
||||
mergeConfig({
|
||||
AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK: '',
|
||||
});
|
||||
const { queryByRole } = render(reduxWrapper(<IntlProgressiveProfilingPage />));
|
||||
const { queryByRole } = render(reduxWrapper(<ProgressiveProfiling />));
|
||||
const button = queryByRole('button', { name: /learn more about how we use this information/i });
|
||||
|
||||
expect(button).toBeNull();
|
||||
@@ -125,7 +133,7 @@ describe('ProgressiveProfilingTests', () => {
|
||||
AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK: 'http://localhost:1999/support',
|
||||
});
|
||||
|
||||
const { getByText } = render(reduxWrapper(<IntlProgressiveProfilingPage />));
|
||||
const { getByText } = render(reduxWrapper(<ProgressiveProfiling />));
|
||||
|
||||
const learnMoreButton = getByText('Learn more about how we use this information.');
|
||||
|
||||
@@ -135,7 +143,7 @@ describe('ProgressiveProfilingTests', () => {
|
||||
it('should open modal on pressing skip for now button', () => {
|
||||
delete window.location;
|
||||
window.location = { href: getConfig().BASE_URL.concat(AUTHN_PROGRESSIVE_PROFILING) };
|
||||
const { getByRole } = render(reduxWrapper(<IntlProgressiveProfilingPage />));
|
||||
const { getByRole } = render(reduxWrapper(<ProgressiveProfiling />));
|
||||
|
||||
const skipButton = getByRole('button', { name: /skip for now/i });
|
||||
fireEvent.click(skipButton);
|
||||
@@ -143,14 +151,15 @@ describe('ProgressiveProfilingTests', () => {
|
||||
const modalContentContainer = document.getElementsByClassName('.pgn__modal-content-container');
|
||||
|
||||
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 ********
|
||||
|
||||
it('should make identify call to segment on progressive profiling page', () => {
|
||||
render(reduxWrapper(<IntlProgressiveProfilingPage />));
|
||||
render(reduxWrapper(<ProgressiveProfiling />));
|
||||
|
||||
expect(identifyAuthenticatedUser).toHaveBeenCalledWith(3);
|
||||
expect(identifyAuthenticatedUser).toHaveBeenCalled();
|
||||
@@ -160,12 +169,12 @@ describe('ProgressiveProfilingTests', () => {
|
||||
mergeConfig({
|
||||
AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK: 'http://localhost:1999/support',
|
||||
});
|
||||
render(reduxWrapper(<IntlProgressiveProfilingPage />));
|
||||
render(reduxWrapper(<ProgressiveProfiling />));
|
||||
|
||||
const supportLink = screen.getByRole('link', { name: /learn more about how we use this information/i });
|
||||
fireEvent.click(supportLink);
|
||||
|
||||
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', () => {
|
||||
@@ -175,10 +184,11 @@ describe('ProgressiveProfilingTests', () => {
|
||||
isLevelOfEducationSelected: false,
|
||||
isWorkExperienceSelected: false,
|
||||
host: '',
|
||||
app_name: APP_NAME,
|
||||
};
|
||||
delete window.location;
|
||||
window.location = { href: getConfig().BASE_URL.concat(AUTHN_PROGRESSIVE_PROFILING) };
|
||||
render(reduxWrapper(<IntlProgressiveProfilingPage />));
|
||||
render(reduxWrapper(<ProgressiveProfiling />));
|
||||
|
||||
const nextButton = screen.getByText('Next');
|
||||
fireEvent.click(nextButton);
|
||||
@@ -194,7 +204,7 @@ describe('ProgressiveProfilingTests', () => {
|
||||
extended_profile: [{ field_name: 'company', field_value: 'test company' }],
|
||||
};
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
const { getByLabelText, getByText } = render(reduxWrapper(<IntlProgressiveProfilingPage />));
|
||||
const { getByLabelText, getByText } = render(reduxWrapper(<ProgressiveProfiling />));
|
||||
|
||||
const genderSelect = getByLabelText('Gender');
|
||||
const companyInput = getByLabelText('Company');
|
||||
@@ -216,7 +226,7 @@ describe('ProgressiveProfilingTests', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const { container } = render(reduxWrapper(<IntlProgressiveProfilingPage />));
|
||||
const { container } = render(reduxWrapper(<ProgressiveProfiling />));
|
||||
const errorElement = container.querySelector('#pp-page-errors');
|
||||
|
||||
expect(errorElement).toBeTruthy();
|
||||
@@ -232,7 +242,7 @@ describe('ProgressiveProfilingTests', () => {
|
||||
href: getConfig().BASE_URL,
|
||||
};
|
||||
|
||||
render(reduxWrapper(<IntlProgressiveProfilingPage />));
|
||||
render(reduxWrapper(<ProgressiveProfiling />));
|
||||
expect(window.location.href).toEqual(DASHBOARD_URL);
|
||||
});
|
||||
|
||||
@@ -249,8 +259,11 @@ describe('ProgressiveProfilingTests', () => {
|
||||
...initialState.welcomePage,
|
||||
success: true,
|
||||
},
|
||||
cohesion: {
|
||||
eventData,
|
||||
},
|
||||
});
|
||||
const { container } = render(reduxWrapper(<IntlProgressiveProfilingPage />));
|
||||
const { container } = render(reduxWrapper(<ProgressiveProfiling />));
|
||||
const nextButton = container.querySelector('button.btn-brand');
|
||||
expect(nextButton.textContent).toEqual('Next');
|
||||
|
||||
@@ -275,13 +288,18 @@ describe('ProgressiveProfilingTests', () => {
|
||||
...initialState.welcomePage,
|
||||
success: true,
|
||||
},
|
||||
cohesion: {
|
||||
eventData,
|
||||
},
|
||||
});
|
||||
|
||||
const { container } = render(reduxWrapper(<IntlProgressiveProfilingPage />));
|
||||
const { container } = render(reduxWrapper(<ProgressiveProfiling />));
|
||||
const nextButton = container.querySelector('button.btn-brand');
|
||||
expect(nextButton.textContent).toEqual('Submit');
|
||||
|
||||
expect(window.location.href).toEqual(redirectUrl);
|
||||
await waitFor(() => {
|
||||
expect(window.location.href).toEqual(redirectUrl);
|
||||
}, { timeout: 1100 });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -311,12 +329,12 @@ describe('ProgressiveProfilingTests', () => {
|
||||
href: getConfig().BASE_URL.concat(AUTHN_PROGRESSIVE_PROFILING),
|
||||
search: `?host=${host}&variant=${EMBEDDED}`,
|
||||
};
|
||||
render(reduxWrapper(<IntlProgressiveProfilingPage />));
|
||||
render(reduxWrapper(<ProgressiveProfiling />));
|
||||
|
||||
const skipLinkButton = screen.getByText('Skip for now');
|
||||
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', () => {
|
||||
@@ -336,7 +354,7 @@ describe('ProgressiveProfilingTests', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const { container } = render(reduxWrapper(<IntlProgressiveProfilingPage />));
|
||||
const { container } = render(reduxWrapper(<ProgressiveProfiling />));
|
||||
|
||||
const tpaSpinnerElement = container.querySelector('#tpa-spinner');
|
||||
expect(tpaSpinnerElement).toBeTruthy();
|
||||
@@ -349,13 +367,14 @@ describe('ProgressiveProfilingTests', () => {
|
||||
isLevelOfEducationSelected: false,
|
||||
isWorkExperienceSelected: false,
|
||||
host: 'http://example.com',
|
||||
app_name: APP_NAME,
|
||||
};
|
||||
delete window.location;
|
||||
window.location = {
|
||||
href: getConfig().BASE_URL.concat(AUTHN_PROGRESSIVE_PROFILING),
|
||||
search: `?host=${host}`,
|
||||
};
|
||||
render(reduxWrapper(<IntlProgressiveProfilingPage />));
|
||||
render(reduxWrapper(<ProgressiveProfiling />));
|
||||
const submitButton = screen.getByText('Next');
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
@@ -370,7 +389,7 @@ describe('ProgressiveProfilingTests', () => {
|
||||
search: `?variant=${EMBEDDED}&host=${host}`,
|
||||
};
|
||||
|
||||
const { container } = render(reduxWrapper(<IntlProgressiveProfilingPage />));
|
||||
const { container } = render(reduxWrapper(<ProgressiveProfiling />));
|
||||
|
||||
const genderField = container.querySelector('#gender');
|
||||
expect(genderField).toBeTruthy();
|
||||
@@ -391,11 +410,11 @@ describe('ProgressiveProfilingTests', () => {
|
||||
},
|
||||
});
|
||||
|
||||
render(reduxWrapper(<IntlProgressiveProfilingPage />));
|
||||
render(reduxWrapper(<ProgressiveProfiling />));
|
||||
expect(window.location.href).toBe(DASHBOARD_URL);
|
||||
});
|
||||
|
||||
it('should redirect to provided redirect url', () => {
|
||||
it('should redirect to provided redirect url', async () => {
|
||||
const redirectUrl = 'https://redirect-test.com';
|
||||
delete window.location;
|
||||
window.location = {
|
||||
@@ -417,12 +436,17 @@ describe('ProgressiveProfilingTests', () => {
|
||||
...initialState.welcomePage,
|
||||
success: true,
|
||||
},
|
||||
cohesion: {
|
||||
eventData,
|
||||
},
|
||||
});
|
||||
|
||||
render(reduxWrapper(<IntlProgressiveProfilingPage />));
|
||||
render(reduxWrapper(<ProgressiveProfiling />));
|
||||
const submitButton = screen.getByText('Submit');
|
||||
fireEvent.click(submitButton);
|
||||
expect(window.location.href).toBe(redirectUrl);
|
||||
await waitFor(() => {
|
||||
expect(window.location.href).toBe(redirectUrl);
|
||||
}, { timeout: 1100 });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
import React from 'react';
|
||||
|
||||
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
import SmallLayout from './SmallLayout';
|
||||
import mockedRecommendedProducts from '../data/tests/mockedData';
|
||||
|
||||
const IntlRecommendationsSmallLayoutPage = injectIntl(SmallLayout);
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useLocation: jest.fn(),
|
||||
@@ -36,7 +32,7 @@ describe('RecommendationsPageTests', () => {
|
||||
});
|
||||
|
||||
it('should render recommendations when recommendations are not loading', () => {
|
||||
const { container } = render(reduxWrapper(<IntlRecommendationsSmallLayoutPage {...props} />));
|
||||
const { container } = render(reduxWrapper(<SmallLayout {...props} />));
|
||||
|
||||
const reactLoadingSkeleton = container.querySelector('.react-loading-skeleton');
|
||||
|
||||
@@ -48,7 +44,7 @@ describe('RecommendationsPageTests', () => {
|
||||
...props,
|
||||
isLoading: true,
|
||||
};
|
||||
const { container } = render(reduxWrapper(<IntlRecommendationsSmallLayoutPage {...props} />));
|
||||
const { container } = render(reduxWrapper(<SmallLayout {...props} />));
|
||||
|
||||
const reactLoadingSkeleton = container.querySelector('.react-loading-skeleton');
|
||||
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { render } from '@testing-library/react';
|
||||
import configureStore from 'redux-mock-store';
|
||||
|
||||
import mockedProductData from './mockedData';
|
||||
import RecommendationList from '../RecommendationsList';
|
||||
|
||||
const IntlRecommendationList = injectIntl(RecommendationList);
|
||||
const mockStore = configureStore();
|
||||
|
||||
describe('RecommendationsListTests', () => {
|
||||
@@ -25,7 +23,7 @@ describe('RecommendationsListTests', () => {
|
||||
userId: 1234567,
|
||||
};
|
||||
|
||||
const { container } = render(reduxWrapper(<IntlRecommendationList {...props} />));
|
||||
const { container } = render(reduxWrapper(<RecommendationList {...props} />));
|
||||
|
||||
const recommendationCards = container.querySelectorAll('.recommendation-card');
|
||||
expect(recommendationCards.length).toEqual(mockedProductData.length);
|
||||
@@ -37,7 +35,7 @@ describe('RecommendationsListTests', () => {
|
||||
userId: 1234567,
|
||||
};
|
||||
|
||||
const { getByText } = render(reduxWrapper(<IntlRecommendationList {...props} />));
|
||||
const { getByText } = render(reduxWrapper(<RecommendationList {...props} />));
|
||||
|
||||
const firstFooterContent = getByText('1 Course');
|
||||
const secondFooterContent = getByText('2 Courses');
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { useMediaQuery } from '@openedx/paragon';
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
@@ -16,7 +15,6 @@ import mockedRecommendedProducts from '../data/tests/mockedData';
|
||||
import RecommendationsPage from '../RecommendationsPage';
|
||||
import { eventNames, getProductMapping } from '../track';
|
||||
|
||||
const IntlRecommendationsPage = injectIntl(RecommendationsPage);
|
||||
const mockStore = configureStore();
|
||||
|
||||
jest.mock('@edx/frontend-platform/analytics', () => ({
|
||||
@@ -77,7 +75,7 @@ describe('RecommendationsPageTests', () => {
|
||||
});
|
||||
|
||||
it('should redirect to dashboard if user is not coming from registration workflow', () => {
|
||||
render(reduxWrapper(<IntlRecommendationsPage />));
|
||||
render(reduxWrapper(<RecommendationsPage />));
|
||||
expect(window.location.href).toEqual(dashboardUrl);
|
||||
});
|
||||
|
||||
@@ -86,14 +84,14 @@ describe('RecommendationsPageTests', () => {
|
||||
recommendations: [],
|
||||
isLoading: false,
|
||||
});
|
||||
render(reduxWrapper(<IntlRecommendationsPage />));
|
||||
render(reduxWrapper(<RecommendationsPage />));
|
||||
expect(window.location.href).toEqual(dashboardUrl);
|
||||
});
|
||||
|
||||
it('should redirect user if they click "Skip for now" button', () => {
|
||||
mockUseLocation();
|
||||
jest.useFakeTimers();
|
||||
const { container } = render(reduxWrapper(<IntlRecommendationsPage />));
|
||||
const { container } = render(reduxWrapper(<RecommendationsPage />));
|
||||
const skipButton = container.querySelector('.pgn__stateful-btn-state-default');
|
||||
fireEvent.click(skipButton);
|
||||
jest.advanceTimersByTime(300);
|
||||
@@ -103,7 +101,7 @@ describe('RecommendationsPageTests', () => {
|
||||
it('should display recommendations small layout for small screen', () => {
|
||||
mockUseLocation();
|
||||
useMediaQuery.mockReturnValue(true);
|
||||
const { container } = render(reduxWrapper(<IntlRecommendationsPage />));
|
||||
const { container } = render(reduxWrapper(<RecommendationsPage />));
|
||||
|
||||
const recommendationsSmallLayout = container.querySelector('#recommendations-small-layout');
|
||||
const reactLoadingSkeleton = container.querySelector('.react-loading-skeleton');
|
||||
@@ -115,7 +113,7 @@ describe('RecommendationsPageTests', () => {
|
||||
it('should display recommendations large layout for large screen', () => {
|
||||
mockUseLocation();
|
||||
useMediaQuery.mockReturnValue(false);
|
||||
const { container } = render(reduxWrapper(<IntlRecommendationsPage />));
|
||||
const { container } = render(reduxWrapper(<RecommendationsPage />));
|
||||
|
||||
const pgnCollapsible = container.querySelector('.pgn_collapsible');
|
||||
const reactLoadingSkeleton = container.querySelector('.react-loading-skeleton');
|
||||
@@ -131,7 +129,7 @@ describe('RecommendationsPageTests', () => {
|
||||
recommendations: [],
|
||||
isLoading: true,
|
||||
});
|
||||
const { container } = render(reduxWrapper(<IntlRecommendationsPage />));
|
||||
const { container } = render(reduxWrapper(<RecommendationsPage />));
|
||||
|
||||
const reactLoadingSkeleton = container.querySelector('.react-loading-skeleton');
|
||||
|
||||
@@ -145,7 +143,7 @@ describe('RecommendationsPageTests', () => {
|
||||
recommendations: [],
|
||||
isLoading: true,
|
||||
});
|
||||
const { container } = render(reduxWrapper(<IntlRecommendationsPage />));
|
||||
const { container } = render(reduxWrapper(<RecommendationsPage />));
|
||||
|
||||
const reactLoadingSkeleton = container.querySelector('.react-loading-skeleton');
|
||||
|
||||
@@ -160,7 +158,7 @@ describe('RecommendationsPageTests', () => {
|
||||
});
|
||||
|
||||
useMediaQuery.mockReturnValue(false);
|
||||
render(reduxWrapper(<IntlRecommendationsPage />));
|
||||
render(reduxWrapper(<RecommendationsPage />));
|
||||
|
||||
expect(sendTrackEvent).toBeCalled();
|
||||
expect(sendTrackEvent).toHaveBeenCalledWith(
|
||||
|
||||
@@ -15,7 +15,7 @@ const generateProductKey = (product) => (
|
||||
export const getProductMapping = (recommendedProducts) => recommendedProducts.map((product) => ({
|
||||
product_key: generateProductKey(product),
|
||||
product_line: product.cardType,
|
||||
product_source: product.productSource.name,
|
||||
product_source: product?.productSource?.name,
|
||||
}));
|
||||
|
||||
export const trackRecommendationClick = (product, position, userId) => {
|
||||
@@ -25,7 +25,7 @@ export const trackRecommendationClick = (product, position, userId) => {
|
||||
recommendation_type: product.recommendationType,
|
||||
product_key: generateProductKey(product),
|
||||
product_line: product.cardType,
|
||||
product_source: product.productSource.name,
|
||||
product_source: product?.productSource?.name,
|
||||
user_id: userId,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { mergeConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import configureStore from 'redux-mock-store';
|
||||
@@ -10,7 +9,6 @@ import configureStore from 'redux-mock-store';
|
||||
import { COUNTRY_CODE_KEY, COUNTRY_DISPLAY_KEY } from './validator';
|
||||
import { CountryField } from '../index';
|
||||
|
||||
const IntlCountryField = injectIntl(CountryField);
|
||||
const mockStore = configureStore();
|
||||
|
||||
jest.mock('react-router-dom', () => {
|
||||
@@ -82,7 +80,7 @@ describe('CountryField', () => {
|
||||
};
|
||||
|
||||
it('should run country field validation when onBlur is fired', () => {
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlCountryField {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<CountryField {...props} />)));
|
||||
const countryInput = container.querySelector('input[name="country"]');
|
||||
|
||||
fireEvent.blur(countryInput, {
|
||||
@@ -97,7 +95,7 @@ describe('CountryField', () => {
|
||||
});
|
||||
|
||||
it('should run country field validation when country name is invalid', () => {
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlCountryField {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<CountryField {...props} />)));
|
||||
const countryInput = container.querySelector('input[name="country"]');
|
||||
|
||||
fireEvent.blur(countryInput, {
|
||||
@@ -112,7 +110,7 @@ describe('CountryField', () => {
|
||||
});
|
||||
|
||||
it('should not run country field validation when onBlur is fired by drop-down arrow icon click', () => {
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlCountryField {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<CountryField {...props} />)));
|
||||
const countryInput = container.querySelector('input[name="country"]');
|
||||
const dropdownArrowIcon = container.querySelector('.btn-icon.pgn__form-autosuggest__icon-button');
|
||||
|
||||
@@ -125,7 +123,7 @@ describe('CountryField', () => {
|
||||
});
|
||||
|
||||
it('should update errors for frontend validations', () => {
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlCountryField {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<CountryField {...props} />)));
|
||||
const countryInput = container.querySelector('input[name="country"]');
|
||||
|
||||
fireEvent.blur(countryInput, { target: { value: '', name: 'country' } });
|
||||
@@ -135,7 +133,7 @@ describe('CountryField', () => {
|
||||
});
|
||||
|
||||
it('should clear error on focus', () => {
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlCountryField {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<CountryField {...props} />)));
|
||||
const countryInput = container.querySelector('input[name="country"]');
|
||||
|
||||
fireEvent.focus(countryInput);
|
||||
@@ -153,7 +151,7 @@ describe('CountryField', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlCountryField {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<CountryField {...props} />)));
|
||||
|
||||
container.querySelector('input[name="country"]');
|
||||
expect(props.onChangeHandler).toHaveBeenCalledTimes(1);
|
||||
@@ -164,7 +162,7 @@ describe('CountryField', () => {
|
||||
});
|
||||
|
||||
it('should set option on dropdown menu item click', () => {
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlCountryField {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<CountryField {...props} />)));
|
||||
|
||||
const dropdownButton = container.querySelector('.pgn__form-autosuggest__icon-button');
|
||||
fireEvent.click(dropdownButton);
|
||||
@@ -181,7 +179,7 @@ describe('CountryField', () => {
|
||||
|
||||
it('should set value on change', () => {
|
||||
const { container } = render(
|
||||
routerWrapper(reduxWrapper(<IntlCountryField {...props} />)),
|
||||
routerWrapper(reduxWrapper(<CountryField {...props} />)),
|
||||
);
|
||||
|
||||
const countryInput = container.querySelector('input[name="country"]');
|
||||
@@ -200,7 +198,7 @@ describe('CountryField', () => {
|
||||
errorMessage: 'country error message',
|
||||
};
|
||||
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlCountryField {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<CountryField {...props} />)));
|
||||
|
||||
const feedbackElement = container.querySelector('div[feedback-for="country"]');
|
||||
expect(feedbackElement).toBeTruthy();
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import configureStore from 'redux-mock-store';
|
||||
@@ -10,7 +9,6 @@ import configureStore from 'redux-mock-store';
|
||||
import { clearRegistrationBackendError, fetchRealtimeValidations } from '../../data/actions';
|
||||
import { EmailField } from '../index';
|
||||
|
||||
const IntlEmailField = injectIntl(EmailField);
|
||||
const mockStore = configureStore();
|
||||
|
||||
jest.mock('react-router-dom', () => {
|
||||
@@ -80,7 +78,7 @@ describe('EmailField', () => {
|
||||
};
|
||||
|
||||
it('should run email field validation when onBlur is fired', () => {
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlEmailField {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<EmailField {...props} />)));
|
||||
|
||||
const emailInput = container.querySelector('input#email');
|
||||
fireEvent.blur(emailInput, { target: { value: '', name: 'email' } });
|
||||
@@ -92,7 +90,7 @@ describe('EmailField', () => {
|
||||
});
|
||||
|
||||
it('should update errors for frontend validations', () => {
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlEmailField {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<EmailField {...props} />)));
|
||||
|
||||
const emailInput = container.querySelector('input#email');
|
||||
fireEvent.blur(emailInput, { target: { value: 'ab', name: 'email' } });
|
||||
@@ -105,7 +103,7 @@ describe('EmailField', () => {
|
||||
});
|
||||
|
||||
it('should clear error on focus', () => {
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlEmailField {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<EmailField {...props} />)));
|
||||
|
||||
const emailInput = container.querySelector('input#email');
|
||||
fireEvent.focus(emailInput, { target: { value: '', name: 'email' } });
|
||||
@@ -119,7 +117,7 @@ describe('EmailField', () => {
|
||||
|
||||
it('should call backend validation api on blur event, if frontend validations have passed', () => {
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlEmailField {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<EmailField {...props} />)));
|
||||
|
||||
// Enter a valid email so that frontend validations are passed
|
||||
const emailInput = container.querySelector('input#email');
|
||||
@@ -129,7 +127,7 @@ describe('EmailField', () => {
|
||||
});
|
||||
|
||||
it('should give email suggestions for common service provider domain typos', () => {
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlEmailField {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<EmailField {...props} />)));
|
||||
|
||||
const emailInput = container.querySelector('input#email');
|
||||
fireEvent.blur(emailInput, { target: { value: 'john@yopmail.com', name: 'email' } });
|
||||
@@ -139,7 +137,7 @@ describe('EmailField', () => {
|
||||
});
|
||||
|
||||
it('should be able to click on email suggestions and set it as value', () => {
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlEmailField {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<EmailField {...props} />)));
|
||||
|
||||
const emailInput = container.querySelector('input#email');
|
||||
fireEvent.blur(emailInput, { target: { value: 'john@yopmail.com', name: 'email' } });
|
||||
@@ -154,7 +152,7 @@ describe('EmailField', () => {
|
||||
});
|
||||
|
||||
it('should give error for common top level domain mistakes', () => {
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlEmailField {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<EmailField {...props} />)));
|
||||
|
||||
const emailInput = container.querySelector('input#email');
|
||||
fireEvent.blur(emailInput, { target: { value: 'john@gmail.mistake', name: 'email' } });
|
||||
@@ -164,7 +162,7 @@ describe('EmailField', () => {
|
||||
});
|
||||
|
||||
it('should give error and suggestion for invalid email', () => {
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlEmailField {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<EmailField {...props} />)));
|
||||
|
||||
const emailInput = container.querySelector('input#email');
|
||||
fireEvent.blur(emailInput, { target: { value: 'john@gmail', name: 'email' } });
|
||||
@@ -194,7 +192,7 @@ describe('EmailField', () => {
|
||||
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlEmailField {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<EmailField {...props} />)));
|
||||
|
||||
const emailInput = container.querySelector('input#email');
|
||||
fireEvent.focus(emailInput, { target: { value: 'a@gmail.com', name: 'email' } });
|
||||
@@ -203,7 +201,7 @@ describe('EmailField', () => {
|
||||
});
|
||||
|
||||
it('should clear email suggestions when close icon is clicked', () => {
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlEmailField {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<EmailField {...props} />)));
|
||||
|
||||
const emailInput = container.querySelector('input#email');
|
||||
fireEvent.blur(emailInput, { target: { value: 'john@gmail.mistake', name: 'email' } });
|
||||
@@ -224,7 +222,7 @@ describe('EmailField', () => {
|
||||
confirmEmailValue: 'confirmEmail@yopmail.com',
|
||||
};
|
||||
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlEmailField {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<EmailField {...props} />)));
|
||||
const emailInput = container.querySelector('input#email');
|
||||
fireEvent.blur(emailInput, { target: { value: 'differentEmail@yopmail.com', name: 'email' } });
|
||||
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig, mergeConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
import { HonorCode } from '../index';
|
||||
|
||||
const IntlHonorCode = injectIntl(HonorCode);
|
||||
|
||||
describe('HonorCodeTest', () => {
|
||||
mergeConfig({
|
||||
PRIVACY_POLICY: 'http://privacy-policy.com',
|
||||
@@ -28,7 +24,7 @@ describe('HonorCodeTest', () => {
|
||||
const errorMessage = `You must agree to the ${getConfig().SITE_NAME} Honor Code`;
|
||||
const { container } = render(
|
||||
<IntlProvider locale="en">
|
||||
<IntlHonorCode
|
||||
<HonorCode
|
||||
errorMessage={errorMessage}
|
||||
onChangeHandler={changeHandler}
|
||||
/>
|
||||
@@ -43,7 +39,7 @@ describe('HonorCodeTest', () => {
|
||||
const expectedMsg = 'I agree to the Your Platform Name Here\u00a0Honor Codein a new tab';
|
||||
const { container } = render(
|
||||
<IntlProvider locale="en">
|
||||
<IntlHonorCode onChangeHandler={changeHandler} />
|
||||
<HonorCode onChangeHandler={changeHandler} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
@@ -56,7 +52,7 @@ describe('HonorCodeTest', () => {
|
||||
it('should render Terms of Service and Honor code field', () => {
|
||||
const { container } = render(
|
||||
<IntlProvider locale="en">
|
||||
<IntlHonorCode fieldType="tos_and_honor_code" onChangeHandler={changeHandler} />
|
||||
<HonorCode fieldType="tos_and_honor_code" onChangeHandler={changeHandler} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
const expectedMsg = 'By creating an account, you agree to the Terms of Service and Honor Code and you '
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import configureStore from 'redux-mock-store';
|
||||
@@ -9,7 +8,6 @@ import configureStore from 'redux-mock-store';
|
||||
import { clearRegistrationBackendError, fetchRealtimeValidations } from '../../data/actions';
|
||||
import { NameField } from '../index';
|
||||
|
||||
const IntlNameField = injectIntl(NameField);
|
||||
const mockStore = configureStore();
|
||||
|
||||
jest.mock('react-router-dom', () => {
|
||||
@@ -69,7 +67,7 @@ describe('NameField', () => {
|
||||
const fieldValidation = { name: 'Enter your full name' };
|
||||
|
||||
it('should run name field validation when onBlur is fired', () => {
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlNameField {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<NameField {...props} />)));
|
||||
|
||||
const nameInput = container.querySelector('input#name');
|
||||
fireEvent.blur(nameInput, { target: { value: '', name: 'name' } });
|
||||
@@ -82,7 +80,7 @@ describe('NameField', () => {
|
||||
});
|
||||
|
||||
it('should update errors for frontend validations', () => {
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlNameField {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<NameField {...props} />)));
|
||||
|
||||
const nameInput = container.querySelector('input#name');
|
||||
fireEvent.blur(nameInput, { target: { value: 'https://invalid-name.com', name: 'name' } });
|
||||
@@ -95,7 +93,7 @@ describe('NameField', () => {
|
||||
});
|
||||
|
||||
it('should clear error on focus', () => {
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlNameField {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<NameField {...props} />)));
|
||||
|
||||
const nameInput = container.querySelector('input#name');
|
||||
fireEvent.focus(nameInput, { target: { value: '', name: 'name' } });
|
||||
@@ -113,7 +111,7 @@ describe('NameField', () => {
|
||||
...props,
|
||||
shouldFetchUsernameSuggestions: true,
|
||||
};
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlNameField {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<NameField {...props} />)));
|
||||
|
||||
const nameInput = container.querySelector('input#name');
|
||||
// Enter a valid name so that frontend validations are passed
|
||||
@@ -135,7 +133,7 @@ describe('NameField', () => {
|
||||
});
|
||||
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlNameField {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<NameField {...props} />)));
|
||||
|
||||
const nameInput = container.querySelector('input#name');
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ export const INVALID_NAME_REGEX = /https?:\/\/(?:[-\w.]|(?:%[\da-fA-F]{2}))*/g;
|
||||
|
||||
const validateName = (value, formatMessage) => {
|
||||
let fieldError = '';
|
||||
if (!value.trim()) {
|
||||
if (!value || (value && !value.trim())) {
|
||||
fieldError = formatMessage(messages['empty.name.field.error']);
|
||||
} else if (URL_REGEX.test(value) || HTML_REGEX.test(value) || INVALID_NAME_REGEX.test(value)) {
|
||||
fieldError = formatMessage(messages['name.validation.message']);
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
|
||||
import { TermsOfService } from '../index';
|
||||
|
||||
const IntlTermsOfService = injectIntl(TermsOfService);
|
||||
|
||||
describe('TermsOfServiceTest', () => {
|
||||
let value = false;
|
||||
|
||||
@@ -23,7 +19,7 @@ describe('TermsOfServiceTest', () => {
|
||||
const errorMessage = `You must agree to the ${getConfig().SITE_NAME} Terms of Service`;
|
||||
const { container } = render(
|
||||
<IntlProvider locale="en">
|
||||
<IntlTermsOfService errorMessage={errorMessage} onChangeHandler={changeHandler} />
|
||||
<TermsOfService errorMessage={errorMessage} onChangeHandler={changeHandler} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
const errorElement = container.querySelector('.form-text-size');
|
||||
@@ -33,7 +29,7 @@ describe('TermsOfServiceTest', () => {
|
||||
it('should render Terms of Service field', () => {
|
||||
const { container } = render(
|
||||
<IntlProvider locale="en">
|
||||
<IntlTermsOfService onChangeHandler={changeHandler} />
|
||||
<TermsOfService onChangeHandler={changeHandler} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
@@ -48,7 +44,7 @@ describe('TermsOfServiceTest', () => {
|
||||
it('should change value when Terms of Service field is checked', () => {
|
||||
const { container } = render(
|
||||
<IntlProvider locale="en">
|
||||
<IntlTermsOfService onChangeHandler={changeHandler} />
|
||||
<TermsOfService onChangeHandler={changeHandler} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
const field = container.querySelector('input#tos');
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import configureStore from 'redux-mock-store';
|
||||
@@ -9,7 +8,6 @@ import configureStore from 'redux-mock-store';
|
||||
import { clearRegistrationBackendError, clearUsernameSuggestions, fetchRealtimeValidations } from '../../data/actions';
|
||||
import { UsernameField } from '../index';
|
||||
|
||||
const IntlUsernameField = injectIntl(UsernameField);
|
||||
const mockStore = configureStore();
|
||||
|
||||
jest.mock('react-router-dom', () => {
|
||||
@@ -73,7 +71,7 @@ describe('UsernameField', () => {
|
||||
};
|
||||
|
||||
it('should run username field validation when onBlur is fired', () => {
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<UsernameField {...props} />)));
|
||||
|
||||
const usernameField = container.querySelector('input#username');
|
||||
fireEvent.blur(usernameField, { target: { value: '', name: 'username' } });
|
||||
@@ -86,7 +84,7 @@ describe('UsernameField', () => {
|
||||
});
|
||||
|
||||
it('should update errors for frontend validations', () => {
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<UsernameField {...props} />)));
|
||||
|
||||
const usernameField = container.querySelector('input#username');
|
||||
fireEvent.blur(usernameField, { target: { value: 'user#', name: 'username' } });
|
||||
@@ -99,7 +97,7 @@ describe('UsernameField', () => {
|
||||
});
|
||||
|
||||
it('should clear error on focus', () => {
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<UsernameField {...props} />)));
|
||||
|
||||
const usernameField = container.querySelector('input#username');
|
||||
fireEvent.focus(usernameField, { target: { value: '', name: 'username' } });
|
||||
@@ -112,7 +110,7 @@ describe('UsernameField', () => {
|
||||
});
|
||||
|
||||
it('should remove space from field on focus if space exists', () => {
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<UsernameField {...props} />)));
|
||||
|
||||
const usernameField = container.querySelector('input#username');
|
||||
fireEvent.focus(usernameField, { target: { value: ' ', name: 'username' } });
|
||||
@@ -125,7 +123,7 @@ describe('UsernameField', () => {
|
||||
|
||||
it('should call backend validation api on blur event, if frontend validations have passed', () => {
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<UsernameField {...props} />)));
|
||||
|
||||
const usernameField = container.querySelector('input#username');
|
||||
// Enter a valid username so that frontend validations are passed
|
||||
@@ -135,7 +133,7 @@ describe('UsernameField', () => {
|
||||
});
|
||||
|
||||
it('should remove space from the start of username on change', () => {
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<UsernameField {...props} />)));
|
||||
const usernameField = container.querySelector('input#username');
|
||||
fireEvent.change(usernameField, { target: { value: ' test-user', name: 'username' } });
|
||||
|
||||
@@ -146,7 +144,7 @@ describe('UsernameField', () => {
|
||||
});
|
||||
|
||||
it('should not set username if it is more than 30 character long', () => {
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<UsernameField {...props} />)));
|
||||
|
||||
const usernameField = container.querySelector('input#username');
|
||||
fireEvent.change(usernameField, { target: { value: 'why_this_is_not_valid_username_', name: 'username' } });
|
||||
@@ -157,7 +155,7 @@ describe('UsernameField', () => {
|
||||
it('should clear username suggestions when username field is focused in', () => {
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<UsernameField {...props} />)));
|
||||
|
||||
const usernameField = container.querySelector('input#username');
|
||||
fireEvent.focus(usernameField);
|
||||
@@ -179,7 +177,7 @@ describe('UsernameField', () => {
|
||||
errorMessage: 'It looks like this username is already taken',
|
||||
};
|
||||
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<UsernameField {...props} />)));
|
||||
const usernameSuggestions = container.querySelectorAll('button.username-suggestions--chip');
|
||||
expect(usernameSuggestions.length).toEqual(3);
|
||||
});
|
||||
@@ -198,7 +196,7 @@ describe('UsernameField', () => {
|
||||
value: ' ',
|
||||
};
|
||||
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<UsernameField {...props} />)));
|
||||
const usernameSuggestions = container.querySelectorAll('button.username-suggestions--chip');
|
||||
expect(usernameSuggestions.length).toEqual(3);
|
||||
});
|
||||
@@ -218,7 +216,7 @@ describe('UsernameField', () => {
|
||||
errorMessage: 'username error',
|
||||
};
|
||||
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<UsernameField {...props} />)));
|
||||
const usernameSuggestions = container.querySelectorAll('button.username-suggestions--chip');
|
||||
expect(usernameSuggestions.length).toEqual(3);
|
||||
});
|
||||
@@ -232,7 +230,7 @@ describe('UsernameField', () => {
|
||||
},
|
||||
});
|
||||
|
||||
render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
|
||||
render(routerWrapper(reduxWrapper(<UsernameField {...props} />)));
|
||||
expect(props.handleChange).toHaveBeenCalledTimes(1);
|
||||
expect(props.handleChange).toHaveBeenCalledWith(
|
||||
{ target: { name: 'username', value: ' ' } },
|
||||
@@ -253,7 +251,7 @@ describe('UsernameField', () => {
|
||||
value: ' ',
|
||||
};
|
||||
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<UsernameField {...props} />)));
|
||||
const usernameSuggestion = container.querySelector('.username-suggestions--chip');
|
||||
fireEvent.click(usernameSuggestion);
|
||||
expect(props.handleChange).toHaveBeenCalledTimes(1);
|
||||
@@ -277,7 +275,7 @@ describe('UsernameField', () => {
|
||||
value: ' ',
|
||||
};
|
||||
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<UsernameField {...props} />)));
|
||||
let closeButton = container.querySelector('button.username-suggestions__close__button');
|
||||
fireEvent.click(closeButton);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(clearUsernameSuggestions());
|
||||
@@ -287,7 +285,7 @@ describe('UsernameField', () => {
|
||||
errorMessage: 'username error',
|
||||
};
|
||||
|
||||
render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
|
||||
render(routerWrapper(reduxWrapper(<UsernameField {...props} />)));
|
||||
closeButton = container.querySelector('button.username-suggestions__close__button');
|
||||
fireEvent.click(closeButton);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(clearUsernameSuggestions());
|
||||
@@ -309,7 +307,7 @@ describe('UsernameField', () => {
|
||||
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<UsernameField {...props} />)));
|
||||
|
||||
const usernameField = container.querySelector('input#username');
|
||||
fireEvent.focus(usernameField, { target: { value: 'test', name: 'username' } });
|
||||
|
||||
@@ -4,7 +4,6 @@ import React, {
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Form, Spinner, StatefulButton } from '@openedx/paragon';
|
||||
import classNames from 'classnames';
|
||||
@@ -12,12 +11,19 @@ import PropTypes from 'prop-types';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import Skeleton from 'react-loading-skeleton';
|
||||
|
||||
import {
|
||||
InstitutionLogistration,
|
||||
PasswordField,
|
||||
RedirectLogistration,
|
||||
ThirdPartyAuthAlert,
|
||||
} from '../common-components';
|
||||
import ConfigurableRegistrationForm from './components/ConfigurableRegistrationForm';
|
||||
import RegistrationFailure from './components/RegistrationFailure';
|
||||
import {
|
||||
backupRegistrationFormBegin,
|
||||
clearRegistrationBackendError,
|
||||
registerNewUser,
|
||||
setAutoGeneratedUsernameExperimentData,
|
||||
setEmailSuggestionInStore,
|
||||
setUserPipelineDataLoaded,
|
||||
} from './data/actions';
|
||||
@@ -25,6 +31,9 @@ import {
|
||||
FORM_SUBMISSION_ERROR,
|
||||
TPA_AUTHENTICATION_FAILURE,
|
||||
} from './data/constants';
|
||||
import useRecaptchaSubmission from './data/hooks';
|
||||
import { AUTO_GENERATED_USERNAME_REGISTRATION_EXP_VARIATION, NOT_INITIALIZED } from './data/optimizelyExperiment/helper';
|
||||
import useAutoGeneratedUsernameExperimentVariation from './data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation';
|
||||
import getBackendValidations from './data/selectors';
|
||||
import {
|
||||
isFormValid, prepareRegistrationPayload,
|
||||
@@ -32,21 +41,20 @@ import {
|
||||
import messages from './messages';
|
||||
import { EmailField, NameField, UsernameField } from './RegistrationFields';
|
||||
import {
|
||||
InstitutionLogistration,
|
||||
PasswordField,
|
||||
RedirectLogistration,
|
||||
ThirdPartyAuthAlert,
|
||||
} from '../common-components';
|
||||
ELEMENT_NAME, ELEMENT_TEXT, ELEMENT_TYPES, PAGE_TYPES,
|
||||
} from '../cohesion/constants';
|
||||
import { setCohesionEventStates } from '../cohesion/data/actions';
|
||||
import { getThirdPartyAuthContext as getRegistrationDataFromBackend } from '../common-components/data/actions';
|
||||
import EnterpriseSSO from '../common-components/EnterpriseSSO';
|
||||
import ThirdPartyAuth from '../common-components/ThirdPartyAuth';
|
||||
import {
|
||||
COMPLETE_STATE, PENDING_STATE, REGISTER_PAGE,
|
||||
APP_NAME, COMPLETE_STATE, PENDING_STATE,
|
||||
REGISTER_PAGE,
|
||||
} from '../data/constants';
|
||||
import {
|
||||
getAllPossibleQueryParams, getTpaHint, getTpaProvider, isHostAvailableInQueryParams, setCookie,
|
||||
getAllPossibleQueryParams, getTpaHint, getTpaProvider, isHostAvailableInQueryParams, removeCookie, setCookie,
|
||||
} from '../data/utils';
|
||||
|
||||
import { trackRegistrationPageViewed, trackRegistrationSuccess } from '../tracking/trackers/register';
|
||||
/**
|
||||
* Main Registration Page component
|
||||
*/
|
||||
@@ -54,6 +62,7 @@ const RegistrationPage = (props) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { executeWithFallback } = useRecaptchaSubmission('submit_registration_form');
|
||||
const registrationEmbedded = isHostAvailableInQueryParams();
|
||||
const platformName = getConfig().SITE_NAME;
|
||||
const flags = {
|
||||
@@ -68,6 +77,7 @@ const RegistrationPage = (props) => {
|
||||
} = props;
|
||||
|
||||
const backedUpFormData = useSelector(state => state.register.registrationFormData);
|
||||
const initExpVariation = useSelector(state => state.register.autoGeneratedUsernameExperimentVariation);
|
||||
const registrationError = useSelector(state => state.register.registrationError);
|
||||
const registrationErrorCode = registrationError?.errorCode;
|
||||
const registrationResult = useSelector(state => state.register.registrationResult);
|
||||
@@ -85,6 +95,7 @@ const RegistrationPage = (props) => {
|
||||
const providers = useSelector(state => state.commonComponents.thirdPartyAuthContext.providers);
|
||||
const secondaryProviders = useSelector(state => state.commonComponents.thirdPartyAuthContext.secondaryProviders);
|
||||
const pipelineUserDetails = useSelector(state => state.commonComponents.thirdPartyAuthContext.pipelineUserDetails);
|
||||
const countriesCodesList = useSelector(state => state.commonComponents.countriesCodesList);
|
||||
|
||||
const backendValidations = useSelector(getBackendValidations);
|
||||
const queryParams = useMemo(() => getAllPossibleQueryParams(), []);
|
||||
@@ -103,6 +114,12 @@ const RegistrationPage = (props) => {
|
||||
? formatMessage(messages['create.account.cta.button'], { label: cta })
|
||||
: 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
|
||||
*/
|
||||
@@ -128,7 +145,7 @@ const RegistrationPage = (props) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (!formStartTime) {
|
||||
sendPageEvent('login_and_registration', 'register');
|
||||
trackRegistrationPageViewed();
|
||||
const payload = { ...queryParams, is_register_page: true };
|
||||
if (tpaHint) {
|
||||
payload.tpa_hint = tpaHint;
|
||||
@@ -149,8 +166,10 @@ const RegistrationPage = (props) => {
|
||||
formFields: { ...formFields },
|
||||
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(() => {
|
||||
if (backendValidations) {
|
||||
@@ -171,10 +190,15 @@ const RegistrationPage = (props) => {
|
||||
useEffect(() => {
|
||||
if (registrationResult.success) {
|
||||
// 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
|
||||
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]);
|
||||
|
||||
@@ -208,15 +232,15 @@ const RegistrationPage = (props) => {
|
||||
}
|
||||
};
|
||||
|
||||
const registerUser = () => {
|
||||
const registerUser = async () => {
|
||||
const totalRegistrationTime = (Date.now() - formStartTime) / 1000;
|
||||
let payload = { ...formFields };
|
||||
let payload = { ...formFields, app_name: APP_NAME };
|
||||
|
||||
if (currentProvider) {
|
||||
delete payload.password;
|
||||
payload.social_auth_provider = currentProvider;
|
||||
}
|
||||
if (flags.autoGeneratedUsernameEnabled) {
|
||||
if (hideUsernameField) {
|
||||
delete payload.username;
|
||||
}
|
||||
|
||||
@@ -228,7 +252,7 @@ const RegistrationPage = (props) => {
|
||||
fieldDescriptions,
|
||||
formatMessage,
|
||||
);
|
||||
setErrors({ ...fieldErrors });
|
||||
setErrors({ ...fieldErrors, captchaError: '' });
|
||||
dispatch(setEmailSuggestionInStore(emailSuggestion));
|
||||
|
||||
// returning if not valid
|
||||
@@ -237,20 +261,41 @@ const RegistrationPage = (props) => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Preparing payload for submission
|
||||
let recaptchaToken = null;
|
||||
try {
|
||||
recaptchaToken = await executeWithFallback();
|
||||
} catch (err) {
|
||||
setErrors(prev => ({
|
||||
...prev,
|
||||
captchaError: err.message,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
payload = prepareRegistrationPayload(
|
||||
payload,
|
||||
configurableFormFields,
|
||||
flags.showMarketingEmailOptInCheckbox,
|
||||
totalRegistrationTime,
|
||||
queryParams);
|
||||
queryParams,
|
||||
);
|
||||
if (recaptchaToken) {
|
||||
payload = { ...payload, captcha_token: recaptchaToken };
|
||||
}
|
||||
|
||||
// making register call
|
||||
dispatch(registerNewUser(payload));
|
||||
};
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
const eventData = {
|
||||
pageType: PAGE_TYPES.ACCOUNT_CREATION,
|
||||
elementType: ELEMENT_TYPES.BUTTON,
|
||||
webElementText: ELEMENT_TEXT.CREATE_ACCOUNT,
|
||||
webElementName: ELEMENT_NAME.CREATE_ACCOUNT,
|
||||
};
|
||||
|
||||
dispatch(setCohesionEventStates(eventData));
|
||||
registerUser();
|
||||
};
|
||||
|
||||
@@ -285,107 +330,117 @@ const RegistrationPage = (props) => {
|
||||
redirectToProgressiveProfilingPage={
|
||||
getConfig().ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN && !!Object.keys(optionalFields.fields).length
|
||||
}
|
||||
currentProvider={currentProvider}
|
||||
/>
|
||||
{autoSubmitRegForm && !errorCode.type ? (
|
||||
<div className="mw-xs mt-5 text-center">
|
||||
<Spinner animation="border" variant="primary" id="tpa-spinner" />
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={classNames(
|
||||
'mw-xs mt-3',
|
||||
{ 'w-100 m-auto pt-4 main-content': registrationEmbedded },
|
||||
)}
|
||||
>
|
||||
<ThirdPartyAuthAlert
|
||||
currentProvider={currentProvider}
|
||||
platformName={platformName}
|
||||
referrer={REGISTER_PAGE}
|
||||
/>
|
||||
<RegistrationFailure
|
||||
errorCode={errorCode.type}
|
||||
failureCount={errorCode.count}
|
||||
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'])}
|
||||
{(autoSubmitRegForm && !errorCode.type)
|
||||
|| (!autoGeneratedUsernameExpVariation && !(
|
||||
autoGeneratedUsernameExpVariation === NOT_INITIALIZED
|
||||
|| registrationEmbedded || !!tpaHint || !!currentProvider))
|
||||
? (
|
||||
<div className="mw-xs mt-5 text-center">
|
||||
<Spinner animation="border" variant="primary" id="tpa-spinner" />
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={classNames(
|
||||
'mw-xs mt-3',
|
||||
{ 'w-100 m-auto pt-4 main-content': registrationEmbedded },
|
||||
)}
|
||||
>
|
||||
<ThirdPartyAuthAlert
|
||||
currentProvider={currentProvider}
|
||||
platformName={platformName}
|
||||
referrer={REGISTER_PAGE}
|
||||
/>
|
||||
<EmailField
|
||||
name="email"
|
||||
value={formFields.email}
|
||||
confirmEmailValue={configurableFormFields?.confirm_email}
|
||||
handleErrorChange={handleErrorChange}
|
||||
handleChange={handleOnChange}
|
||||
errorMessage={errors.email}
|
||||
helpText={[formatMessage(messages['help.text.email'])]}
|
||||
floatingLabel={formatMessage(messages['registration.email.label'])}
|
||||
<RegistrationFailure
|
||||
errorCode={errorCode.type}
|
||||
failureCount={errorCode.count}
|
||||
context={{ provider: currentProvider, errorMessage: thirdPartyAuthErrorMessage }}
|
||||
/>
|
||||
{!flags.autoGeneratedUsernameEnabled && (
|
||||
<UsernameField
|
||||
name="username"
|
||||
spellCheck="false"
|
||||
value={formFields.username}
|
||||
<Form id="registration-form" name="registration-form">
|
||||
<NameField
|
||||
name="name"
|
||||
value={formFields.name}
|
||||
shouldFetchUsernameSuggestions={!formFields.username.trim()}
|
||||
handleChange={handleOnChange}
|
||||
handleErrorChange={handleErrorChange}
|
||||
errorMessage={errors.username}
|
||||
helpText={[formatMessage(messages['help.text.username.1']), formatMessage(messages['help.text.username.2'])]}
|
||||
floatingLabel={formatMessage(messages['registration.username.label'])}
|
||||
errorMessage={errors.name}
|
||||
helpText={[formatMessage(messages['help.text.name'])]}
|
||||
floatingLabel={formatMessage(messages['registration.fullname.label'])}
|
||||
/>
|
||||
)}
|
||||
{!currentProvider && (
|
||||
<PasswordField
|
||||
name="password"
|
||||
value={formFields.password}
|
||||
handleChange={handleOnChange}
|
||||
<EmailField
|
||||
name="email"
|
||||
value={formFields.email}
|
||||
confirmEmailValue={configurableFormFields?.confirm_email}
|
||||
handleErrorChange={handleErrorChange}
|
||||
errorMessage={errors.password}
|
||||
floatingLabel={formatMessage(messages['registration.password.label'])}
|
||||
handleChange={handleOnChange}
|
||||
errorMessage={errors.email}
|
||||
helpText={[formatMessage(messages['help.text.email'])]}
|
||||
floatingLabel={formatMessage(messages['registration.email.label'])}
|
||||
/>
|
||||
)}
|
||||
<ConfigurableRegistrationForm
|
||||
email={formFields.email}
|
||||
fieldErrors={errors}
|
||||
formFields={configurableFormFields}
|
||||
setFieldErrors={registrationEmbedded ? setTemporaryErrors : setErrors}
|
||||
setFormFields={setConfigurableFormFields}
|
||||
autoSubmitRegisterForm={autoSubmitRegForm}
|
||||
fieldDescriptions={fieldDescriptions}
|
||||
/>
|
||||
<StatefulButton
|
||||
id="register-user"
|
||||
name="register-user"
|
||||
type="submit"
|
||||
variant="brand"
|
||||
className="register-button mt-4 mb-4"
|
||||
state={submitState}
|
||||
labels={{
|
||||
default: buttonLabel,
|
||||
pending: '',
|
||||
}}
|
||||
onClick={handleSubmit}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
/>
|
||||
{!registrationEmbedded && (
|
||||
<ThirdPartyAuth
|
||||
currentProvider={currentProvider}
|
||||
providers={providers}
|
||||
secondaryProviders={secondaryProviders}
|
||||
handleInstitutionLogin={handleInstitutionLogin}
|
||||
thirdPartyAuthApiStatus={thirdPartyAuthApiStatus}
|
||||
{!hideUsernameField && (
|
||||
<UsernameField
|
||||
name="username"
|
||||
spellCheck="false"
|
||||
value={formFields.username}
|
||||
handleChange={handleOnChange}
|
||||
handleErrorChange={handleErrorChange}
|
||||
errorMessage={errors.username}
|
||||
helpText={[formatMessage(messages['help.text.username.1']), formatMessage(messages['help.text.username.2'])]}
|
||||
floatingLabel={formatMessage(messages['registration.username.label'])}
|
||||
/>
|
||||
)}
|
||||
{!currentProvider && (
|
||||
<PasswordField
|
||||
name="password"
|
||||
value={formFields.password}
|
||||
handleChange={handleOnChange}
|
||||
handleErrorChange={handleErrorChange}
|
||||
errorMessage={errors.password}
|
||||
floatingLabel={formatMessage(messages['registration.password.label'])}
|
||||
/>
|
||||
)}
|
||||
<ConfigurableRegistrationForm
|
||||
email={formFields.email}
|
||||
fieldErrors={errors}
|
||||
formFields={configurableFormFields}
|
||||
setFieldErrors={registrationEmbedded ? setTemporaryErrors : setErrors}
|
||||
setFormFields={setConfigurableFormFields}
|
||||
autoSubmitRegisterForm={autoSubmitRegForm}
|
||||
fieldDescriptions={fieldDescriptions}
|
||||
countriesCodesList={countriesCodesList}
|
||||
/>
|
||||
)}
|
||||
</Form>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{errors?.captchaError && (
|
||||
<div className="mt-3 pgn__form-text-invalid pgn__form-text">
|
||||
{errors.captchaError}
|
||||
</div>
|
||||
)}
|
||||
<StatefulButton
|
||||
id="register-user"
|
||||
name="register-user"
|
||||
type="submit"
|
||||
variant="brand"
|
||||
className="register-button mt-4 mb-4"
|
||||
state={submitState}
|
||||
labels={{
|
||||
default: buttonLabel,
|
||||
pending: '',
|
||||
}}
|
||||
onClick={handleSubmit}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
/>
|
||||
{!registrationEmbedded && (
|
||||
<ThirdPartyAuth
|
||||
currentProvider={currentProvider}
|
||||
providers={providers}
|
||||
secondaryProviders={secondaryProviders}
|
||||
handleInstitutionLogin={handleInstitutionLogin}
|
||||
thirdPartyAuthApiStatus={thirdPartyAuthApiStatus}
|
||||
/>
|
||||
)}
|
||||
</Form>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { getConfig, mergeConfig } from '@edx/frontend-platform';
|
||||
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import {
|
||||
configure, getLocale, injectIntl, IntlProvider,
|
||||
configure, getLocale, IntlProvider,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import { fireEvent, render, waitFor } from '@testing-library/react';
|
||||
import { mockNavigate, BrowserRouter as Router } from 'react-router-dom';
|
||||
import configureStore from 'redux-mock-store';
|
||||
|
||||
@@ -17,9 +16,14 @@ import {
|
||||
setUserPipelineDataLoaded,
|
||||
} from './data/actions';
|
||||
import { INTERNAL_SERVER_ERROR } from './data/constants';
|
||||
import mockTagular from '../cohesion/utils';
|
||||
import useRecaptchaSubmission from './data/hooks';
|
||||
import { NOT_INITIALIZED } from './data/optimizelyExperiment/helper';
|
||||
import useAutoGeneratedUsernameExperimentVariation
|
||||
from './data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation';
|
||||
import RegistrationPage from './RegistrationPage';
|
||||
import {
|
||||
AUTHN_PROGRESSIVE_PROFILING, COMPLETE_STATE, PENDING_STATE, REGISTER_PAGE,
|
||||
APP_NAME, AUTHN_PROGRESSIVE_PROFILING, COMPLETE_STATE, PENDING_STATE, REGISTER_PAGE,
|
||||
} from '../data/constants';
|
||||
|
||||
jest.mock('@edx/frontend-platform/analytics', () => ({
|
||||
@@ -30,9 +34,16 @@ jest.mock('@edx/frontend-platform/i18n', () => ({
|
||||
...jest.requireActual('@edx/frontend-platform/i18n'),
|
||||
getLocale: jest.fn(),
|
||||
}));
|
||||
jest.mock('./data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation', () => jest.fn());
|
||||
jest.mock('./data/hooks', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(() => ({
|
||||
executeWithFallback: jest.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
const IntlRegistrationPage = injectIntl(RegistrationPage);
|
||||
const mockStore = configureStore();
|
||||
mockTagular();
|
||||
|
||||
jest.mock('react-router-dom', () => {
|
||||
const mockNavigation = jest.fn();
|
||||
@@ -102,6 +113,7 @@ describe('RegistrationPage', () => {
|
||||
usernameSuggestions: [],
|
||||
|
||||
},
|
||||
cohesion: { eventData: {} },
|
||||
commonComponents: {
|
||||
thirdPartyAuthApiStatus: null,
|
||||
thirdPartyAuthContext,
|
||||
@@ -128,6 +140,12 @@ describe('RegistrationPage', () => {
|
||||
institutionLogin: false,
|
||||
};
|
||||
window.location = { search: '' };
|
||||
useAutoGeneratedUsernameExperimentVariation.mockReturnValue(NOT_INITIALIZED);
|
||||
useRecaptchaSubmission.mockReturnValue({
|
||||
executeWithFallback: jest.fn().mockResolvedValue(null),
|
||||
isReady: true,
|
||||
isLoading: false,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -169,7 +187,7 @@ describe('RegistrationPage', () => {
|
||||
|
||||
// ******** test registration form submission ********
|
||||
|
||||
it('should submit form for valid input', () => {
|
||||
it('should submit form for valid input', async () => {
|
||||
getLocale.mockImplementation(() => ('en-us'));
|
||||
jest.spyOn(global.Date, 'now').mockImplementation(() => 0);
|
||||
|
||||
@@ -185,18 +203,21 @@ describe('RegistrationPage', () => {
|
||||
honor_code: true,
|
||||
total_registration_time: 0,
|
||||
next: '/course/demo-course-url',
|
||||
app_name: APP_NAME,
|
||||
};
|
||||
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
|
||||
populateRequiredFields(getByLabelText, payload);
|
||||
const button = container.querySelector('button.btn-brand');
|
||||
fireEvent.click(button);
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK' }));
|
||||
await waitFor(() => {
|
||||
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK' }));
|
||||
});
|
||||
});
|
||||
|
||||
it('should submit form without password field when current provider is present', () => {
|
||||
it('should submit form without password field when current provider is present', async () => {
|
||||
jest.spyOn(global.Date, 'now').mockImplementation(() => 0);
|
||||
|
||||
const formPayload = {
|
||||
@@ -207,6 +228,7 @@ describe('RegistrationPage', () => {
|
||||
honor_code: true,
|
||||
social_auth_provider: 'Apple',
|
||||
total_registration_time: 0,
|
||||
app_name: APP_NAME,
|
||||
};
|
||||
|
||||
store = mockStore({
|
||||
@@ -220,12 +242,14 @@ describe('RegistrationPage', () => {
|
||||
},
|
||||
});
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
|
||||
|
||||
populateRequiredFields(getByLabelText, formPayload, true);
|
||||
const button = container.querySelector('button.btn-brand');
|
||||
fireEvent.click(button);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...formPayload, country: 'PK' }));
|
||||
await waitFor(() => {
|
||||
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...formPayload, country: 'PK' }));
|
||||
});
|
||||
});
|
||||
|
||||
it('should display an error when form is submitted with an invalid email', () => {
|
||||
@@ -243,7 +267,7 @@ describe('RegistrationPage', () => {
|
||||
};
|
||||
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
|
||||
populateRequiredFields(getByLabelText, formPayload, true);
|
||||
|
||||
const button = container.querySelector('button.btn-brand');
|
||||
@@ -268,7 +292,7 @@ describe('RegistrationPage', () => {
|
||||
};
|
||||
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
|
||||
populateRequiredFields(getByLabelText, formPayload, true);
|
||||
const button = container.querySelector('button.btn-brand');
|
||||
fireEvent.click(button);
|
||||
@@ -276,7 +300,7 @@ describe('RegistrationPage', () => {
|
||||
expect(validationErrors.textContent).toContain(usernameError);
|
||||
});
|
||||
|
||||
it('should submit form with marketing email opt in value', () => {
|
||||
it('should submit form with marketing email opt in value', async () => {
|
||||
mergeConfig({
|
||||
MARKETING_EMAILS_OPT_IN: 'true',
|
||||
});
|
||||
@@ -292,21 +316,24 @@ describe('RegistrationPage', () => {
|
||||
honor_code: true,
|
||||
total_registration_time: 0,
|
||||
marketing_emails_opt_in: true,
|
||||
app_name: APP_NAME,
|
||||
};
|
||||
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
|
||||
populateRequiredFields(getByLabelText, payload);
|
||||
const button = container.querySelector('button.btn-brand');
|
||||
fireEvent.click(button);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK' }));
|
||||
await waitFor(() => {
|
||||
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK' }));
|
||||
});
|
||||
|
||||
mergeConfig({
|
||||
MARKETING_EMAILS_OPT_IN: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('should submit form without UsernameField when autoGeneratedUsernameEnabled is true', () => {
|
||||
it('should submit form without UsernameField when autoGeneratedUsernameEnabled is true', async () => {
|
||||
mergeConfig({
|
||||
ENABLE_AUTO_GENERATED_USERNAME: true,
|
||||
});
|
||||
@@ -318,14 +345,17 @@ describe('RegistrationPage', () => {
|
||||
country: 'Pakistan',
|
||||
honor_code: true,
|
||||
total_registration_time: 0,
|
||||
app_name: APP_NAME,
|
||||
};
|
||||
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
|
||||
populateRequiredFields(getByLabelText, payload, false, true);
|
||||
const button = container.querySelector('button.btn-brand');
|
||||
fireEvent.click(button);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK' }));
|
||||
await waitFor(() => {
|
||||
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK' }));
|
||||
});
|
||||
mergeConfig({
|
||||
ENABLE_AUTO_GENERATED_USERNAME: false,
|
||||
});
|
||||
@@ -336,7 +366,7 @@ describe('RegistrationPage', () => {
|
||||
ENABLE_AUTO_GENERATED_USERNAME: true,
|
||||
});
|
||||
|
||||
const { queryByLabelText } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
const { queryByLabelText } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
|
||||
expect(queryByLabelText('Username')).toBeNull();
|
||||
|
||||
mergeConfig({
|
||||
@@ -347,7 +377,7 @@ describe('RegistrationPage', () => {
|
||||
it('should not dispatch registerNewUser on empty form Submission', () => {
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
|
||||
|
||||
const button = container.querySelector('button.btn-brand');
|
||||
fireEvent.click(button);
|
||||
@@ -357,8 +387,126 @@ describe('RegistrationPage', () => {
|
||||
|
||||
// ******** test registration form validations ********
|
||||
|
||||
it('should submit form with valid reCAPTCHA token', async () => {
|
||||
getLocale.mockImplementation(() => ('en-us'));
|
||||
jest.spyOn(global.Date, 'now').mockImplementation(() => 0);
|
||||
|
||||
useRecaptchaSubmission.mockReturnValue({
|
||||
executeWithFallback: jest.fn().mockResolvedValue('mock-recaptcha-token'),
|
||||
isReady: true,
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
const payload = {
|
||||
name: 'John Doe',
|
||||
username: 'john_doe',
|
||||
email: 'john.doe@gmail.com',
|
||||
password: 'password1',
|
||||
country: 'Pakistan',
|
||||
honor_code: true,
|
||||
total_registration_time: 0,
|
||||
app_name: APP_NAME,
|
||||
};
|
||||
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
|
||||
populateRequiredFields(getByLabelText, payload);
|
||||
|
||||
const button = container.querySelector('button.btn-brand');
|
||||
fireEvent.click(button);
|
||||
|
||||
await waitFor(() => {
|
||||
const actions = store.dispatch.mock.calls.map(call => call[0]);
|
||||
const registerAction = actions.find(a => a.type === registerNewUser().type);
|
||||
|
||||
expect(registerAction).toBeTruthy();
|
||||
expect(registerAction.payload).toMatchObject({
|
||||
registrationInfo: {
|
||||
...payload,
|
||||
country: 'PK',
|
||||
captcha_token: 'mock-recaptcha-token',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should display error when reCAPTCHA verification fails', async () => {
|
||||
getLocale.mockImplementation(() => ('en-us'));
|
||||
jest.spyOn(global.Date, 'now').mockImplementation(() => 0);
|
||||
|
||||
useRecaptchaSubmission.mockReturnValue({
|
||||
executeWithFallback: jest.fn().mockRejectedValue(new Error('CAPTCHA verification failed.')),
|
||||
isReady: true,
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
const payload = {
|
||||
name: 'John Doe',
|
||||
username: 'john_doe',
|
||||
email: 'john.doe@gmail.com',
|
||||
password: 'password1',
|
||||
country: 'Pakistan',
|
||||
honor_code: true,
|
||||
total_registration_time: 0,
|
||||
};
|
||||
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
|
||||
populateRequiredFields(getByLabelText, payload);
|
||||
|
||||
const button = container.querySelector('button.btn-brand');
|
||||
fireEvent.click(button);
|
||||
|
||||
await waitFor(() => {
|
||||
const captchaError = container.querySelector('.pgn__form-text-invalid');
|
||||
expect(captchaError.textContent).toContain('CAPTCHA verification failed.');
|
||||
});
|
||||
|
||||
expect(store.dispatch).not.toHaveBeenCalledWith(expect.objectContaining({
|
||||
type: registerNewUser().type,
|
||||
}));
|
||||
});
|
||||
|
||||
it('should submit without reCAPTCHA token if reCAPTCHA is disabled', async () => {
|
||||
getLocale.mockImplementation(() => ('en-us'));
|
||||
jest.spyOn(global.Date, 'now').mockImplementation(() => 0);
|
||||
|
||||
useRecaptchaSubmission.mockReturnValue({
|
||||
executeWithFallback: jest.fn().mockResolvedValue(null),
|
||||
isReady: true,
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
const payload = {
|
||||
name: 'John Doe',
|
||||
username: 'john_doe',
|
||||
email: 'john.doe@gmail.com',
|
||||
password: 'password1',
|
||||
country: 'Pakistan',
|
||||
honor_code: true,
|
||||
total_registration_time: 0,
|
||||
app_name: APP_NAME,
|
||||
};
|
||||
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
|
||||
populateRequiredFields(getByLabelText, payload);
|
||||
|
||||
const button = container.querySelector('button.btn-brand');
|
||||
fireEvent.click(button);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(store.dispatch).toHaveBeenCalledWith(
|
||||
registerNewUser({
|
||||
...payload,
|
||||
country: 'PK',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should show error messages for required fields on empty form submission', () => {
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
|
||||
|
||||
const button = container.querySelector('button.btn-brand');
|
||||
fireEvent.click(button);
|
||||
@@ -386,7 +534,7 @@ describe('RegistrationPage', () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlProvider locale="en"><IntlRegistrationPage {...props} /></IntlProvider>)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlProvider locale="en"><RegistrationPage {...props} /></IntlProvider>)));
|
||||
const usernameFeedback = container.querySelector('div[feedback-for="username"]');
|
||||
const emailFeedback = container.querySelector('div[feedback-for="email"]');
|
||||
|
||||
@@ -395,7 +543,7 @@ describe('RegistrationPage', () => {
|
||||
});
|
||||
|
||||
it('should clear error on focus', () => {
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
|
||||
|
||||
const submitButton = container.querySelector('button.btn-brand');
|
||||
fireEvent.click(submitButton);
|
||||
@@ -424,7 +572,7 @@ describe('RegistrationPage', () => {
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
|
||||
const { container } = render(routerWrapper(reduxWrapper(
|
||||
<IntlRegistrationPage {...props} />,
|
||||
<RegistrationPage {...props} />,
|
||||
)));
|
||||
|
||||
const emailInput = container.querySelector('input#email');
|
||||
@@ -435,7 +583,7 @@ describe('RegistrationPage', () => {
|
||||
// ******** test form buttons and fields ********
|
||||
|
||||
it('should match default button state', () => {
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
|
||||
const button = container.querySelector('button[type="submit"] span');
|
||||
expect(button.textContent).toEqual('Create an account for free');
|
||||
});
|
||||
@@ -449,7 +597,7 @@ describe('RegistrationPage', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
|
||||
|
||||
const button = container.querySelector('button[type="submit"] span.sr-only');
|
||||
expect(button.textContent).toEqual('pending');
|
||||
@@ -460,7 +608,7 @@ describe('RegistrationPage', () => {
|
||||
MARKETING_EMAILS_OPT_IN: 'true',
|
||||
});
|
||||
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
|
||||
const checkboxDivs = container.querySelectorAll('div.form-field--checkbox');
|
||||
expect(checkboxDivs.length).toEqual(1);
|
||||
|
||||
@@ -473,7 +621,7 @@ describe('RegistrationPage', () => {
|
||||
const buttonLabel = 'Register';
|
||||
delete window.location;
|
||||
window.location = { href: getConfig().BASE_URL, search: `?cta=${buttonLabel}` };
|
||||
const { container } = render(reduxWrapper(<IntlRegistrationPage {...props} />));
|
||||
const { container } = render(reduxWrapper(<RegistrationPage {...props} />));
|
||||
const button = container.querySelector('button[type="submit"] span');
|
||||
|
||||
const buttonText = button.textContent;
|
||||
@@ -492,11 +640,11 @@ describe('RegistrationPage', () => {
|
||||
},
|
||||
});
|
||||
|
||||
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
|
||||
expect(document.cookie).toMatch(`${getConfig().USER_RETENTION_COOKIE_NAME}=true`);
|
||||
});
|
||||
|
||||
it('should redirect to url returned in registration result after successful account creation', () => {
|
||||
it('should redirect to url returned in registration result after successful account creation', async () => {
|
||||
const dashboardURL = 'https://test.com/testing-dashboard/';
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
@@ -510,11 +658,13 @@ describe('RegistrationPage', () => {
|
||||
});
|
||||
delete window.location;
|
||||
window.location = { href: getConfig().BASE_URL };
|
||||
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
expect(window.location.href).toBe(dashboardURL);
|
||||
render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
|
||||
await waitFor(() => {
|
||||
expect(window.location.href).toBe(dashboardURL);
|
||||
}, { timeout: 1100 });
|
||||
});
|
||||
|
||||
it('should redirect to dashboard if features flags are configured but no optional fields are configured', () => {
|
||||
it('should redirect to dashboard if features flags are configured but no optional fields are configured', async () => {
|
||||
mergeConfig({
|
||||
ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN: true,
|
||||
});
|
||||
@@ -537,8 +687,10 @@ describe('RegistrationPage', () => {
|
||||
});
|
||||
delete window.location;
|
||||
window.location = { href: getConfig().BASE_URL };
|
||||
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
expect(window.location.href).toBe(dashboardUrl);
|
||||
render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
|
||||
await waitFor(() => {
|
||||
expect(window.location.href).toBe(dashboardUrl);
|
||||
}, { timeout: 1100 });
|
||||
});
|
||||
|
||||
it('should redirect to progressive profiling page if optional fields are configured', () => {
|
||||
@@ -568,7 +720,7 @@ describe('RegistrationPage', () => {
|
||||
|
||||
render(reduxWrapper(
|
||||
<Router>
|
||||
<IntlRegistrationPage {...props} />
|
||||
<RegistrationPage {...props} />
|
||||
</Router>,
|
||||
));
|
||||
expect(mockNavigate).toHaveBeenCalledWith(AUTHN_PROGRESSIVE_PROFILING);
|
||||
@@ -586,13 +738,13 @@ describe('RegistrationPage', () => {
|
||||
});
|
||||
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
|
||||
expect(store.dispatch).toHaveBeenCalledWith(backupRegistrationFormBegin({ ...registrationFormData }));
|
||||
});
|
||||
|
||||
it('should send page event when register page is rendered', () => {
|
||||
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
expect(sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'register');
|
||||
render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
|
||||
expect(sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'register', { app_name: APP_NAME });
|
||||
});
|
||||
|
||||
it('should send track event when user has successfully registered', () => {
|
||||
@@ -609,8 +761,8 @@ describe('RegistrationPage', () => {
|
||||
|
||||
delete window.location;
|
||||
window.location = { href: getConfig().BASE_URL };
|
||||
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.user.account.registered.client', {});
|
||||
render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
|
||||
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.user.account.registered.client', { app_name: APP_NAME });
|
||||
});
|
||||
|
||||
it('should populate form with pipeline user details', () => {
|
||||
@@ -635,7 +787,7 @@ describe('RegistrationPage', () => {
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
const { container } = render(reduxWrapper(
|
||||
<Router>
|
||||
<IntlRegistrationPage {...props} />
|
||||
<RegistrationPage {...props} />
|
||||
</Router>,
|
||||
));
|
||||
|
||||
@@ -658,7 +810,7 @@ describe('RegistrationPage', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
|
||||
const validationErrors = container.querySelector('div#validation-errors');
|
||||
expect(validationErrors.textContent).toContain(
|
||||
'An error has occurred. Try refreshing the page, or check your internet connection.',
|
||||
@@ -685,7 +837,7 @@ describe('RegistrationPage', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
|
||||
|
||||
const fullNameInput = container.querySelector('input#name');
|
||||
const usernameInput = container.querySelector('input#username');
|
||||
@@ -731,14 +883,14 @@ describe('RegistrationPage', () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
|
||||
expect(window.parent.postMessage).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should not display validations error on blur event when embedded variant is rendered', () => {
|
||||
delete window.location;
|
||||
window.location = { href: getConfig().BASE_URL.concat(REGISTER_PAGE), search: '?host=http://localhost/host-website' };
|
||||
const { container } = render(reduxWrapper(<IntlRegistrationPage {...props} />));
|
||||
const { container } = render(reduxWrapper(<RegistrationPage {...props} />));
|
||||
|
||||
const usernameInput = container.querySelector('input#username');
|
||||
fireEvent.blur(usernameInput, { target: { value: '', name: 'username' } });
|
||||
@@ -766,7 +918,7 @@ describe('RegistrationPage', () => {
|
||||
},
|
||||
});
|
||||
const { container } = render(routerWrapper(reduxWrapper(
|
||||
<IntlRegistrationPage {...props} />),
|
||||
<RegistrationPage {...props} />),
|
||||
));
|
||||
|
||||
const usernameFeedback = container.querySelector('div[feedback-for="username"]');
|
||||
@@ -783,7 +935,7 @@ describe('RegistrationPage', () => {
|
||||
search: '?host=http://localhost/host-website',
|
||||
};
|
||||
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
|
||||
const submitButton = container.querySelector('button.btn-brand');
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
@@ -824,7 +976,7 @@ describe('RegistrationPage', () => {
|
||||
});
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
|
||||
const spinnerElement = container.querySelector('#tpa-spinner');
|
||||
const registrationFormElement = container.querySelector('#registration-form');
|
||||
|
||||
@@ -832,7 +984,7 @@ describe('RegistrationPage', () => {
|
||||
expect(registrationFormElement).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should auto register if autoSubmitRegForm is true and pipeline details are loaded', () => {
|
||||
it('should auto register if autoSubmitRegForm is true and pipeline details are loaded', async () => {
|
||||
jest.spyOn(global.Date, 'now').mockImplementation(() => 0);
|
||||
getLocale.mockImplementation(() => ('en-us'));
|
||||
|
||||
@@ -875,15 +1027,18 @@ describe('RegistrationPage', () => {
|
||||
});
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
|
||||
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({
|
||||
name: 'John Doe',
|
||||
username: 'john_doe',
|
||||
email: 'john.doe@example.com',
|
||||
country: 'PK',
|
||||
social_auth_provider: 'Apple',
|
||||
total_registration_time: 0,
|
||||
}));
|
||||
render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
|
||||
await waitFor(() => {
|
||||
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({
|
||||
name: 'John Doe',
|
||||
username: 'john_doe',
|
||||
email: 'john.doe@example.com',
|
||||
country: 'PK',
|
||||
social_auth_provider: 'Apple',
|
||||
total_registration_time: 0,
|
||||
app_name: APP_NAME,
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getCountryList, getLocale, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {
|
||||
ELEMENT_NAME, ELEMENT_TEXT, ELEMENT_TYPES, PAGE_TYPES,
|
||||
} from '../../cohesion/constants';
|
||||
import trackCohesionEvent from '../../cohesion/trackers';
|
||||
import { FormFieldRenderer } from '../../field-renderer';
|
||||
import { backupRegistrationFormBegin } from '../data/actions';
|
||||
import { FIELDS } from '../data/constants';
|
||||
import messages from '../messages';
|
||||
import { CountryField, HonorCode, TermsOfService } from '../RegistrationFields';
|
||||
@@ -31,13 +37,14 @@ const ConfigurableRegistrationForm = (props) => {
|
||||
setFieldErrors,
|
||||
setFormFields,
|
||||
autoSubmitRegistrationForm,
|
||||
countriesCodesList,
|
||||
} = props;
|
||||
const dispatch = useDispatch();
|
||||
|
||||
/** The reason for adding the entry 'United States' is that Chrome browser aut-fill the form with the 'Unites
|
||||
States' instead of 'United States of America' which does not exist in country dropdown list and gets the user
|
||||
confused and unable to create an account. So we added the United States entry in the dropdown list.
|
||||
*/
|
||||
const countryList = useMemo(() => getCountryList(getLocale()).concat([{ code: 'US', name: 'United States' }]), []);
|
||||
|
||||
let showTermsOfServiceAndHonorCode = false;
|
||||
let showCountryField = false;
|
||||
@@ -50,6 +57,8 @@ const ConfigurableRegistrationForm = (props) => {
|
||||
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.
|
||||
*/
|
||||
@@ -70,6 +79,16 @@ const ConfigurableRegistrationForm = (props) => {
|
||||
}
|
||||
}, [autoSubmitRegistrationForm]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const removeDisabledCountries = useCallback((countryList) => {
|
||||
if (!countriesCodesList.length) {
|
||||
return countryList;
|
||||
}
|
||||
return countryList.filter(({ code }) => countriesCodesList.find(x => x === code));
|
||||
}, [countriesCodesList]);
|
||||
|
||||
const countryList = useMemo(() => removeDisabledCountries(
|
||||
getCountryList(getLocale()).concat([{ code: 'US', name: 'United States' }])), [removeDisabledCountries]);
|
||||
|
||||
const handleErrorChange = (fieldName, error) => {
|
||||
if (fieldName) {
|
||||
setFieldErrors(prevErrors => ({
|
||||
@@ -90,6 +109,25 @@ const ConfigurableRegistrationForm = (props) => {
|
||||
setFieldErrors(prevErrors => ({ ...prevErrors, [name]: '' }));
|
||||
}
|
||||
}
|
||||
// setting marketingEmailsOptIn state for SSO authentication flow for register API call
|
||||
if (name === 'marketingEmailsOptIn') {
|
||||
if (!value) {
|
||||
const cohesionEventData = {
|
||||
pageType: PAGE_TYPES.ACCOUNT_CREATION,
|
||||
elementType: ELEMENT_TYPES.BUTTON,
|
||||
webElementText: ELEMENT_TEXT.OPT_IN_TEXT,
|
||||
webElementName: ELEMENT_NAME.OPT_OUT,
|
||||
};
|
||||
trackCohesionEvent(cohesionEventData);
|
||||
}
|
||||
dispatch(backupRegistrationFormBegin({
|
||||
...backedUpFormData,
|
||||
configurableFormFields: {
|
||||
...backedUpFormData.configurableFormFields,
|
||||
[name]: value,
|
||||
},
|
||||
}));
|
||||
}
|
||||
setFormFields(prevState => ({ ...prevState, [name]: value }));
|
||||
};
|
||||
|
||||
@@ -231,11 +269,16 @@ ConfigurableRegistrationForm.propTypes = {
|
||||
setFieldErrors: PropTypes.func.isRequired,
|
||||
setFormFields: PropTypes.func.isRequired,
|
||||
autoSubmitRegistrationForm: PropTypes.bool,
|
||||
countriesCodesList: PropTypes.arrayOf(PropTypes.shape({
|
||||
code: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
})),
|
||||
};
|
||||
|
||||
ConfigurableRegistrationForm.defaultProps = {
|
||||
fieldDescriptions: {},
|
||||
autoSubmitRegistrationForm: false,
|
||||
countriesCodesList: [],
|
||||
};
|
||||
|
||||
export default ConfigurableRegistrationForm;
|
||||
|
||||
@@ -9,6 +9,7 @@ import PropTypes from 'prop-types';
|
||||
import { windowScrollTo } from '../../data/utils';
|
||||
import {
|
||||
FORBIDDEN_REQUEST,
|
||||
FORBIDDEN_USERNAME,
|
||||
INTERNAL_SERVER_ERROR,
|
||||
TPA_AUTHENTICATION_FAILURE,
|
||||
TPA_SESSION_EXPIRED,
|
||||
@@ -48,6 +49,9 @@ const RegistrationFailureMessage = (props) => {
|
||||
case TPA_SESSION_EXPIRED:
|
||||
errorMessage = formatMessage(messages['registration.tpa.session.expired'], { provider: context.provider });
|
||||
break;
|
||||
case FORBIDDEN_USERNAME:
|
||||
errorMessage = formatMessage(messages['registration.forbidden.username']);
|
||||
break;
|
||||
default:
|
||||
errorMessage = formatMessage(messages['registration.empty.form.submission.error']);
|
||||
break;
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { mergeConfig } from '@edx/frontend-platform';
|
||||
import {
|
||||
getLocale, injectIntl, IntlProvider,
|
||||
getLocale, IntlProvider,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import { fireEvent, render, waitFor } from '@testing-library/react';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import configureStore from 'redux-mock-store';
|
||||
|
||||
import { APP_NAME } from '../../../data/constants';
|
||||
import { registerNewUser } from '../../data/actions';
|
||||
import { FIELDS } from '../../data/constants';
|
||||
import { NOT_INITIALIZED } from '../../data/optimizelyExperiment/helper';
|
||||
import useAutoGeneratedUsernameExperimentVariation
|
||||
from '../../data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation';
|
||||
import RegistrationPage from '../../RegistrationPage';
|
||||
import ConfigurableRegistrationForm from '../ConfigurableRegistrationForm';
|
||||
|
||||
@@ -22,9 +25,8 @@ jest.mock('@edx/frontend-platform/i18n', () => ({
|
||||
...jest.requireActual('@edx/frontend-platform/i18n'),
|
||||
getLocale: jest.fn(),
|
||||
}));
|
||||
jest.mock('../../data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation', () => jest.fn());
|
||||
|
||||
const IntlConfigurableRegistrationForm = injectIntl(ConfigurableRegistrationForm);
|
||||
const IntlRegistrationPage = injectIntl(RegistrationPage);
|
||||
const mockStore = configureStore();
|
||||
|
||||
jest.mock('react-router-dom', () => {
|
||||
@@ -94,6 +96,7 @@ describe('ConfigurableRegistrationForm', () => {
|
||||
registrationFormData,
|
||||
usernameSuggestions: [],
|
||||
},
|
||||
cohesion: { eventData: {} },
|
||||
commonComponents: {
|
||||
thirdPartyAuthApiStatus: null,
|
||||
thirdPartyAuthContext,
|
||||
@@ -121,6 +124,7 @@ describe('ConfigurableRegistrationForm', () => {
|
||||
};
|
||||
window.location = { search: '' };
|
||||
getLocale.mockImplementationOnce(() => ('en-us'));
|
||||
useAutoGeneratedUsernameExperimentVariation.mockReturnValue(NOT_INITIALIZED);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -158,7 +162,7 @@ describe('ConfigurableRegistrationForm', () => {
|
||||
};
|
||||
|
||||
render(routerWrapper(reduxWrapper(
|
||||
<IntlConfigurableRegistrationForm {...props} />,
|
||||
<ConfigurableRegistrationForm {...props} />,
|
||||
)));
|
||||
|
||||
expect(document.querySelector('#profession')).toBeTruthy();
|
||||
@@ -185,10 +189,11 @@ describe('ConfigurableRegistrationForm', () => {
|
||||
},
|
||||
},
|
||||
autoSubmitRegistrationForm: true,
|
||||
countriesCodesList: [{ code: 'AX', name: 'Åland Islands' }, { code: 'AL', name: 'Albania' }],
|
||||
};
|
||||
|
||||
render(routerWrapper(reduxWrapper(
|
||||
<IntlConfigurableRegistrationForm {...props} />,
|
||||
<ConfigurableRegistrationForm {...props} />,
|
||||
)));
|
||||
|
||||
expect(props.setFormFields).toHaveBeenCalledTimes(2);
|
||||
@@ -215,12 +220,12 @@ describe('ConfigurableRegistrationForm', () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
|
||||
expect(document.querySelector('#profession')).toBeTruthy();
|
||||
expect(document.querySelector('#tos')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should submit form with fields returned by backend in payload', () => {
|
||||
it('should submit form with fields returned by backend in payload', async () => {
|
||||
mergeConfig({
|
||||
SHOW_CONFIGURABLE_EDX_FIELDS: true,
|
||||
});
|
||||
@@ -249,7 +254,7 @@ describe('ConfigurableRegistrationForm', () => {
|
||||
};
|
||||
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
|
||||
|
||||
populateRequiredFields(getByLabelText, payload);
|
||||
|
||||
@@ -260,7 +265,9 @@ describe('ConfigurableRegistrationForm', () => {
|
||||
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK' }));
|
||||
await waitFor(() => {
|
||||
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK', app_name: APP_NAME }));
|
||||
});
|
||||
});
|
||||
|
||||
it('should show error messages for required fields on empty form submission', () => {
|
||||
@@ -284,7 +291,7 @@ describe('ConfigurableRegistrationForm', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
|
||||
const submitButton = container.querySelector('button.btn-brand');
|
||||
|
||||
fireEvent.click(submitButton);
|
||||
@@ -310,7 +317,7 @@ describe('ConfigurableRegistrationForm', () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
|
||||
const countryInput = container.querySelector('input[name="country"]');
|
||||
fireEvent.change(countryInput, { target: { value: 'Pak', name: 'country' } });
|
||||
fireEvent.blur(countryInput, { target: { value: 'Pak', name: 'country' } });
|
||||
@@ -335,7 +342,7 @@ describe('ConfigurableRegistrationForm', () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
|
||||
|
||||
const emailInput = getByLabelText('Email');
|
||||
const confirmEmailInput = getByLabelText('Confirm Email');
|
||||
@@ -371,7 +378,7 @@ describe('ConfigurableRegistrationForm', () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
|
||||
|
||||
populateRequiredFields(getByLabelText, formPayload, true);
|
||||
fireEvent.change(
|
||||
@@ -406,7 +413,7 @@ describe('ConfigurableRegistrationForm', () => {
|
||||
});
|
||||
|
||||
const { getByLabelText, container } = render(
|
||||
routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)),
|
||||
routerWrapper(reduxWrapper(<RegistrationPage {...props} />)),
|
||||
);
|
||||
|
||||
const professionInput = getByLabelText('Profession');
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { mergeConfig } from '@edx/frontend-platform';
|
||||
import {
|
||||
configure, getLocale, injectIntl, IntlProvider,
|
||||
configure, getLocale, IntlProvider,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
@@ -12,6 +11,9 @@ import configureStore from 'redux-mock-store';
|
||||
import {
|
||||
FORBIDDEN_REQUEST, INTERNAL_SERVER_ERROR, TPA_AUTHENTICATION_FAILURE, TPA_SESSION_EXPIRED,
|
||||
} from '../../data/constants';
|
||||
import { NOT_INITIALIZED } from '../../data/optimizelyExperiment/helper';
|
||||
import useAutoGeneratedUsernameExperimentVariation
|
||||
from '../../data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation';
|
||||
import RegistrationPage from '../../RegistrationPage';
|
||||
import RegistrationFailureMessage from '../RegistrationFailure';
|
||||
|
||||
@@ -23,9 +25,8 @@ jest.mock('@edx/frontend-platform/i18n', () => ({
|
||||
...jest.requireActual('@edx/frontend-platform/i18n'),
|
||||
getLocale: jest.fn(),
|
||||
}));
|
||||
jest.mock('../../data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation', () => jest.fn());
|
||||
|
||||
const IntlRegistrationPage = injectIntl(RegistrationPage);
|
||||
const IntlRegistrationFailure = injectIntl(RegistrationFailureMessage);
|
||||
const mockStore = configureStore();
|
||||
|
||||
jest.mock('react-router-dom', () => {
|
||||
@@ -95,6 +96,7 @@ describe('RegistrationFailure', () => {
|
||||
registrationFormData,
|
||||
usernameSuggestions: [],
|
||||
},
|
||||
cohesion: { eventData: {} },
|
||||
commonComponents: {
|
||||
thirdPartyAuthApiStatus: null,
|
||||
thirdPartyAuthContext,
|
||||
@@ -121,6 +123,7 @@ describe('RegistrationFailure', () => {
|
||||
institutionLogin: false,
|
||||
};
|
||||
window.location = { search: '' };
|
||||
useAutoGeneratedUsernameExperimentVariation.mockReturnValue(NOT_INITIALIZED);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -137,7 +140,7 @@ describe('RegistrationFailure', () => {
|
||||
failureCount: 0,
|
||||
};
|
||||
|
||||
const { container } = render(reduxWrapper(<IntlRegistrationFailure {...props} />));
|
||||
const { container } = render(reduxWrapper(<RegistrationFailureMessage {...props} />));
|
||||
|
||||
const alertHeading = container.querySelectorAll('div.alert-heading');
|
||||
expect(alertHeading.length).toEqual(1);
|
||||
@@ -153,7 +156,7 @@ describe('RegistrationFailure', () => {
|
||||
failureCount: 0,
|
||||
};
|
||||
|
||||
const { container } = render(reduxWrapper(<IntlRegistrationFailure {...props} />));
|
||||
const { container } = render(reduxWrapper(<RegistrationFailureMessage {...props} />));
|
||||
|
||||
const alertHeading = container.querySelectorAll('div.alert-heading');
|
||||
expect(alertHeading.length).toEqual(1);
|
||||
@@ -172,7 +175,7 @@ describe('RegistrationFailure', () => {
|
||||
failureCount: 0,
|
||||
};
|
||||
|
||||
const { container } = render(reduxWrapper(<IntlRegistrationFailure {...props} />));
|
||||
const { container } = render(reduxWrapper(<RegistrationFailureMessage {...props} />));
|
||||
|
||||
const alertHeading = container.querySelectorAll('div.alert-heading');
|
||||
expect(alertHeading.length).toEqual(1);
|
||||
@@ -191,7 +194,7 @@ describe('RegistrationFailure', () => {
|
||||
failureCount: 0,
|
||||
};
|
||||
|
||||
const { container } = render(reduxWrapper(<IntlRegistrationFailure {...props} />));
|
||||
const { container } = render(reduxWrapper(<RegistrationFailureMessage {...props} />));
|
||||
|
||||
const alertHeading = container.querySelectorAll('div.alert-heading');
|
||||
expect(alertHeading.length).toEqual(1);
|
||||
@@ -211,7 +214,7 @@ describe('RegistrationFailure', () => {
|
||||
},
|
||||
});
|
||||
|
||||
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
|
||||
const validationError = screen.queryByText('An error has occurred. Try refreshing the page, or check your internet connection.');
|
||||
|
||||
expect(validationError).not.toBeNull();
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { getConfig, mergeConfig } from '@edx/frontend-platform';
|
||||
import {
|
||||
configure, getLocale, injectIntl, IntlProvider,
|
||||
configure, getLocale, IntlProvider,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import { fireEvent, render, waitFor } from '@testing-library/react';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import configureStore from 'redux-mock-store';
|
||||
|
||||
import mockTagular from '../../../cohesion/utils';
|
||||
import {
|
||||
COMPLETE_STATE, LOGIN_PAGE, PENDING_STATE, REGISTER_PAGE,
|
||||
} from '../../../data/constants';
|
||||
import { NOT_INITIALIZED } from '../../data/optimizelyExperiment/helper';
|
||||
import useAutoGeneratedUsernameExperimentVariation
|
||||
from '../../data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation';
|
||||
import RegistrationPage from '../../RegistrationPage';
|
||||
|
||||
jest.mock('@edx/frontend-platform/analytics', () => ({
|
||||
@@ -22,8 +25,9 @@ jest.mock('@edx/frontend-platform/i18n', () => ({
|
||||
...jest.requireActual('@edx/frontend-platform/i18n'),
|
||||
getLocale: jest.fn(),
|
||||
}));
|
||||
jest.mock('../../data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation', () => jest.fn());
|
||||
mockTagular();
|
||||
|
||||
const IntlRegistrationPage = injectIntl(RegistrationPage);
|
||||
const mockStore = configureStore();
|
||||
|
||||
jest.mock('react-router-dom', () => {
|
||||
@@ -94,6 +98,7 @@ describe('ThirdPartyAuth', () => {
|
||||
registrationFormData,
|
||||
usernameSuggestions: [],
|
||||
},
|
||||
cohesion: { eventData: {} },
|
||||
commonComponents: {
|
||||
thirdPartyAuthApiStatus: null,
|
||||
thirdPartyAuthContext,
|
||||
@@ -120,6 +125,7 @@ describe('ThirdPartyAuth', () => {
|
||||
institutionLogin: false,
|
||||
};
|
||||
window.location = { search: '' };
|
||||
useAutoGeneratedUsernameExperimentVariation.mockReturnValue(NOT_INITIALIZED);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -157,7 +163,7 @@ describe('ThirdPartyAuth', () => {
|
||||
});
|
||||
|
||||
const { queryByLabelText } = render(
|
||||
routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />, { store })),
|
||||
routerWrapper(reduxWrapper(<RegistrationPage {...props} />, { store })),
|
||||
);
|
||||
|
||||
const passwordField = queryByLabelText('Password');
|
||||
@@ -182,7 +188,7 @@ describe('ThirdPartyAuth', () => {
|
||||
window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: `?next=/dashboard&tpa_hint=${ssoProvider.id}` };
|
||||
|
||||
const { container } = render(
|
||||
routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)),
|
||||
routerWrapper(reduxWrapper(<RegistrationPage {...props} />)),
|
||||
);
|
||||
const tpaButton = container.querySelector(`button#${ssoProvider.id}`);
|
||||
|
||||
@@ -207,7 +213,7 @@ describe('ThirdPartyAuth', () => {
|
||||
search: `?next=/dashboard&tpa_hint=${ssoProvider.id}`,
|
||||
};
|
||||
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
|
||||
const skeletonElement = container.querySelector('.react-loading-skeleton');
|
||||
|
||||
expect(skeletonElement).toBeTruthy();
|
||||
@@ -231,7 +237,7 @@ describe('ThirdPartyAuth', () => {
|
||||
window.location = { href: getConfig().BASE_URL.concat(REGISTER_PAGE), search: `?next=/dashboard&tpa_hint=${ssoProvider.id}` };
|
||||
ssoProvider.iconImage = null;
|
||||
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
|
||||
const iconElement = container.querySelector(`button#${ssoProvider.id} div span.pgn__icon`);
|
||||
|
||||
expect(iconElement).toBeTruthy();
|
||||
@@ -254,7 +260,7 @@ describe('ThirdPartyAuth', () => {
|
||||
delete window.location;
|
||||
window.location = { href: getConfig().BASE_URL.concat(REGISTER_PAGE), search: `?next=/dashboard&tpa_hint=${secondaryProviders.id}` };
|
||||
|
||||
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
|
||||
expect(window.location.href).toEqual(getConfig().LMS_BASE_URL + secondaryProviders.registerUrl);
|
||||
});
|
||||
|
||||
@@ -275,7 +281,7 @@ describe('ThirdPartyAuth', () => {
|
||||
delete window.location;
|
||||
window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: '?next=/dashboard&tpa_hint=invalid' };
|
||||
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
|
||||
const providerButton = container.querySelector(`button#${ssoProvider.id} span#provider-name`);
|
||||
|
||||
expect(providerButton.textContent).toEqual(expectedMessage);
|
||||
@@ -294,7 +300,7 @@ describe('ThirdPartyAuth', () => {
|
||||
});
|
||||
|
||||
const { container } = render(
|
||||
routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />, { store })),
|
||||
routerWrapper(reduxWrapper(<RegistrationPage {...props} />, { store })),
|
||||
);
|
||||
|
||||
const buttonsWithId = container.querySelectorAll(`button#${ssoProvider.id}`);
|
||||
@@ -315,7 +321,7 @@ describe('ThirdPartyAuth', () => {
|
||||
});
|
||||
|
||||
const { container } = render(
|
||||
routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)),
|
||||
routerWrapper(reduxWrapper(<RegistrationPage {...props} />)),
|
||||
);
|
||||
|
||||
const buttonsWithId = container.querySelectorAll(`button#${ssoProvider.id}`);
|
||||
@@ -329,12 +335,12 @@ describe('ThirdPartyAuth', () => {
|
||||
institutionLogin: true,
|
||||
};
|
||||
|
||||
const { getByText } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
const { getByText } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
|
||||
const headingElement = getByText('Register with institution/campus credentials');
|
||||
expect(headingElement).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should redirect to social auth provider url on SSO button click', () => {
|
||||
it('should redirect to social auth provider url on SSO button click', async () => {
|
||||
const registerUrl = '/auth/login/apple-id/?auth_entry=register&next=/dashboard';
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
@@ -354,16 +360,18 @@ describe('ThirdPartyAuth', () => {
|
||||
window.location = { href: getConfig().BASE_URL };
|
||||
|
||||
const { container } = render(
|
||||
routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)),
|
||||
routerWrapper(reduxWrapper(<RegistrationPage {...props} />)),
|
||||
);
|
||||
|
||||
const ssoButton = container.querySelector('button#oa2-apple-id');
|
||||
fireEvent.click(ssoButton);
|
||||
|
||||
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + registerUrl);
|
||||
await waitFor(() => {
|
||||
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + registerUrl);
|
||||
}, { timeout: 1100 });
|
||||
});
|
||||
|
||||
it('should redirect to finishAuthUrl upon successful registration via SSO', () => {
|
||||
it('should redirect to finishAuthUrl upon successful registration via SSO', async () => {
|
||||
const authCompleteUrl = '/auth/complete/google-oauth2/';
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
@@ -385,8 +393,10 @@ describe('ThirdPartyAuth', () => {
|
||||
delete window.location;
|
||||
window.location = { href: getConfig().BASE_URL };
|
||||
|
||||
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + authCompleteUrl);
|
||||
render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
|
||||
await waitFor(() => {
|
||||
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + authCompleteUrl);
|
||||
}, { timeout: 1100 });
|
||||
});
|
||||
|
||||
// ******** test alert messages ********
|
||||
@@ -406,7 +416,7 @@ describe('ThirdPartyAuth', () => {
|
||||
const expectedMessage = `${'You\'ve successfully signed into Apple! We just need a little more information before '
|
||||
+ 'you start learning with '}${ getConfig().SITE_NAME }.`;
|
||||
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<RegistrationPage {...props} />)));
|
||||
const tpaAlert = container.querySelector('#tpa-alert p');
|
||||
expect(tpaAlert.textContent).toEqual(expectedMessage);
|
||||
});
|
||||
@@ -437,7 +447,7 @@ describe('ThirdPartyAuth', () => {
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
|
||||
const { container } = render(
|
||||
routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)),
|
||||
routerWrapper(reduxWrapper(<RegistrationPage {...props} />)),
|
||||
);
|
||||
|
||||
const alertHeading = container.querySelector('div.alert-heading');
|
||||
|
||||
@@ -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_USER_PIPELINE_DATA_LOADED = 'REGISTER_SET_USER_PIPELINE_DATA_LOADED';
|
||||
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
|
||||
export const backupRegistrationForm = () => ({
|
||||
type: BACKUP_REGISTRATION_DATA.BASE,
|
||||
@@ -83,3 +83,9 @@ export const setUserPipelineDataLoaded = (value) => ({
|
||||
type: REGISTER_SET_USER_PIPELINE_DATA_LOADED,
|
||||
payload: { value },
|
||||
});
|
||||
|
||||
// Auto Generated Username Registration Experiment Actions
|
||||
export const setAutoGeneratedUsernameExperimentData = (autoGeneratedRegExpVariation) => ({
|
||||
type: REGISTER_SET_AUTO_GENERATED_USERNAME_REGISTRATION_EXP_DATA,
|
||||
payload: { autoGeneratedRegExpVariation },
|
||||
});
|
||||
|
||||
@@ -11,3 +11,4 @@ export const FORM_SUBMISSION_ERROR = 'form-submission-error';
|
||||
export const INTERNAL_SERVER_ERROR = 'internal-server-error';
|
||||
export const TPA_AUTHENTICATION_FAILURE = 'tpa-authentication-failure';
|
||||
export const TPA_SESSION_EXPIRED = 'tpa-session-expired';
|
||||
export const FORBIDDEN_USERNAME = 'forbidden-username';
|
||||
|
||||
40
src/register/data/hooks.js
Normal file
40
src/register/data/hooks.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useGoogleReCaptcha } from 'react-google-recaptcha-v3';
|
||||
|
||||
import messages from '../messages';
|
||||
|
||||
const useRecaptchaSubmission = (actionName = 'submit') => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { executeRecaptcha } = useGoogleReCaptcha();
|
||||
const recaptchaKey = getConfig().RECAPTCHA_SITE_KEY_WEB;
|
||||
|
||||
const isReady = !!executeRecaptcha || !recaptchaKey;
|
||||
|
||||
const executeWithFallback = useCallback(async () => {
|
||||
if (executeRecaptcha && recaptchaKey) {
|
||||
const token = await executeRecaptcha(actionName);
|
||||
if (!token) {
|
||||
throw new Error(formatMessage(messages['registration.captcha.verification.label']));
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
// Fallback: no reCAPTCHA or not ready
|
||||
if (recaptchaKey) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(`reCAPTCHA not ready for action: ${actionName}. Proceeding without token.`);
|
||||
}
|
||||
return null;
|
||||
}, [executeRecaptcha, recaptchaKey, actionName, formatMessage]);
|
||||
|
||||
return {
|
||||
executeWithFallback,
|
||||
isReady,
|
||||
isLoading: recaptchaKey && !executeRecaptcha,
|
||||
};
|
||||
};
|
||||
|
||||
export default useRecaptchaSubmission;
|
||||
90
src/register/data/hooks.test.jsx
Normal file
90
src/register/data/hooks.test.jsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { useGoogleReCaptcha } from 'react-google-recaptcha-v3';
|
||||
|
||||
import useRecaptchaSubmission from './hooks';
|
||||
|
||||
jest.mock('@edx/frontend-platform', () => ({
|
||||
getConfig: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('react-google-recaptcha-v3', () => ({
|
||||
useGoogleReCaptcha: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@edx/frontend-platform/i18n', () => ({
|
||||
...jest.requireActual('@edx/frontend-platform/i18n'),
|
||||
useIntl: () => ({ formatMessage: (msg) => msg.defaultMessage || msg }),
|
||||
}));
|
||||
|
||||
describe('useRecaptchaSubmission', () => {
|
||||
beforeEach(() => {
|
||||
getConfig.mockReturnValue({ RECAPTCHA_SITE_KEY_WEB: 'test-key' });
|
||||
useGoogleReCaptcha.mockReturnValue({ executeRecaptcha: jest.fn() });
|
||||
});
|
||||
|
||||
it('should throw error if reCAPTCHA returns empty token', async () => {
|
||||
useGoogleReCaptcha.mockReturnValue({
|
||||
executeRecaptcha: jest.fn().mockResolvedValue(null),
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useRecaptchaSubmission('test_action'), {
|
||||
wrapper: ({ children }) => <IntlProvider locale="en">{children}</IntlProvider>,
|
||||
});
|
||||
|
||||
await expect(result.current.executeWithFallback()).rejects.toThrow(
|
||||
'CAPTCHA verification failed.',
|
||||
);
|
||||
});
|
||||
|
||||
it('should warn and return null if reCAPTCHA key exists but executeRecaptcha is not ready', async () => {
|
||||
const warnSpy = jest.spyOn(console, 'warn').mockImplementation();
|
||||
|
||||
useGoogleReCaptcha.mockReturnValue({
|
||||
executeRecaptcha: undefined,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useRecaptchaSubmission('test_action'), {
|
||||
wrapper: ({ children }) => <IntlProvider locale="en">{children}</IntlProvider>,
|
||||
});
|
||||
|
||||
const token = await result.current.executeWithFallback();
|
||||
|
||||
expect(token).toBeNull();
|
||||
expect(warnSpy).toHaveBeenCalledWith(
|
||||
'reCAPTCHA not ready for action: test_action. Proceeding without token.',
|
||||
);
|
||||
|
||||
warnSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should handle undefined RECAPTCHA_SITE_KEY_WEB gracefully', async () => {
|
||||
const warnSpy = jest.spyOn(console, 'warn').mockImplementation();
|
||||
|
||||
getConfig.mockReturnValue({ RECAPTCHA_SITE_KEY_WEB: undefined });
|
||||
|
||||
const { result } = renderHook(() => useRecaptchaSubmission('test_action'), {
|
||||
wrapper: ({ children }) => <IntlProvider locale="en">{children}</IntlProvider>,
|
||||
});
|
||||
|
||||
const token = await result.current.executeWithFallback();
|
||||
|
||||
expect(token).toBeNull();
|
||||
expect(warnSpy).not.toHaveBeenCalled();
|
||||
warnSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should return token if reCAPTCHA succeeds', async () => {
|
||||
useGoogleReCaptcha.mockReturnValue({
|
||||
executeRecaptcha: jest.fn().mockResolvedValue('valid-token'),
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useRecaptchaSubmission('test_action'), {
|
||||
wrapper: ({ children }) => <IntlProvider locale="en">{children}</IntlProvider>,
|
||||
});
|
||||
|
||||
const token = await result.current.executeWithFallback();
|
||||
expect(token).toBe('valid-token');
|
||||
});
|
||||
});
|
||||
30
src/register/data/optimizelyExperiment/helper.js
Normal file
30
src/register/data/optimizelyExperiment/helper.js
Normal 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,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/* eslint-disable consistent-return */
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
REGISTER_CLEAR_USERNAME_SUGGESTIONS,
|
||||
REGISTER_FORM_VALIDATIONS,
|
||||
REGISTER_NEW_USER,
|
||||
REGISTER_SET_AUTO_GENERATED_USERNAME_REGISTRATION_EXP_DATA,
|
||||
REGISTER_SET_COUNTRY_CODE,
|
||||
REGISTER_SET_EMAIL_SUGGESTIONS,
|
||||
REGISTER_SET_USER_PIPELINE_DATA_LOADED,
|
||||
@@ -39,6 +40,7 @@ export const defaultState = {
|
||||
usernameSuggestions: [],
|
||||
validationApiRateLimited: false,
|
||||
shouldBackupState: false,
|
||||
autoGeneratedUsernameExperimentVariation: '',
|
||||
};
|
||||
|
||||
const reducer = (state = defaultState, action = {}) => {
|
||||
@@ -55,6 +57,12 @@ const reducer = (state = defaultState, action = {}) => {
|
||||
registrationFormData: { ...action.payload },
|
||||
userPipelineDataLoaded: state.userPipelineDataLoaded,
|
||||
};
|
||||
case REGISTER_SET_AUTO_GENERATED_USERNAME_REGISTRATION_EXP_DATA: {
|
||||
return {
|
||||
...state,
|
||||
autoGeneratedUsernameExperimentVariation: action.payload.autoGeneratedRegExpVariation,
|
||||
};
|
||||
}
|
||||
case REGISTER_NEW_USER.BEGIN:
|
||||
return {
|
||||
...state,
|
||||
|
||||
@@ -38,6 +38,7 @@ describe('Registration Reducer Tests', () => {
|
||||
usernameSuggestions: [],
|
||||
validationApiRateLimited: false,
|
||||
shouldBackupState: false,
|
||||
autoGeneratedUsernameExperimentVariation: '',
|
||||
};
|
||||
|
||||
it('should return the initial state', () => {
|
||||
|
||||
@@ -43,39 +43,31 @@ export const isFormValid = (
|
||||
Object.keys(payload).forEach(key => {
|
||||
switch (key) {
|
||||
case 'name':
|
||||
if (!fieldErrors.name) {
|
||||
fieldErrors.name = validateName(payload.name, formatMessage);
|
||||
}
|
||||
fieldErrors.name = validateName(payload.name, formatMessage);
|
||||
if (fieldErrors.name) { isValid = false; }
|
||||
break;
|
||||
case 'email': {
|
||||
if (!fieldErrors.email) {
|
||||
const {
|
||||
fieldError, confirmEmailError, suggestion,
|
||||
} = validateEmail(payload.email, configurableFormFields?.confirm_email, formatMessage);
|
||||
if (fieldError) {
|
||||
fieldErrors.email = fieldError;
|
||||
isValid = false;
|
||||
}
|
||||
if (confirmEmailError) {
|
||||
fieldErrors.confirm_email = confirmEmailError;
|
||||
isValid = false;
|
||||
}
|
||||
emailSuggestion = suggestion;
|
||||
const {
|
||||
fieldError, confirmEmailError, suggestion,
|
||||
} = validateEmail(payload.email, configurableFormFields?.confirm_email, formatMessage);
|
||||
if (fieldError) {
|
||||
fieldErrors.email = fieldError;
|
||||
isValid = false;
|
||||
}
|
||||
if (confirmEmailError) {
|
||||
fieldErrors.confirm_email = confirmEmailError;
|
||||
isValid = false;
|
||||
}
|
||||
emailSuggestion = suggestion;
|
||||
if (fieldErrors.email) { isValid = false; }
|
||||
break;
|
||||
}
|
||||
case 'username':
|
||||
if (!fieldErrors.username) {
|
||||
fieldErrors.username = validateUsername(payload.username, formatMessage);
|
||||
}
|
||||
fieldErrors.username = validateUsername(payload.username, formatMessage);
|
||||
if (fieldErrors.username) { isValid = false; }
|
||||
break;
|
||||
case 'password':
|
||||
if (!fieldErrors.password) {
|
||||
fieldErrors.password = validatePasswordField(payload.password, formatMessage);
|
||||
}
|
||||
fieldErrors.password = validatePasswordField(payload.password, formatMessage);
|
||||
if (fieldErrors.password) { isValid = false; }
|
||||
break;
|
||||
default:
|
||||
|
||||
@@ -162,6 +162,11 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Registration using {provider} has timed out.',
|
||||
description: '',
|
||||
},
|
||||
'registration.forbidden.username': {
|
||||
id: 'registration.forbidden.username',
|
||||
defaultMessage: 'Usernames can\'t include words that could be mistaken for course roles. Please choose a different username.',
|
||||
description: '',
|
||||
},
|
||||
'registration.tpa.authentication.failure': {
|
||||
id: 'registration.tpa.authentication.failure',
|
||||
defaultMessage: 'We are sorry, you are not authorized to access {platform_name} via this channel. '
|
||||
@@ -201,6 +206,11 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Did you mean',
|
||||
description: 'Did you mean alert suggestion',
|
||||
},
|
||||
'registration.captcha.verification.label': {
|
||||
id: 'registration.captcha.verification.label',
|
||||
defaultMessage: 'CAPTCHA verification failed.',
|
||||
description: 'CAPTCHA verification failed',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -18,7 +18,7 @@ import { useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
import { resetPassword, validateToken } from './data/actions';
|
||||
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';
|
||||
import { resetPasswordResultSelector } from './data/selectors';
|
||||
import { validatePassword } from './data/service';
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
LETTER_REGEX, LOGIN_PAGE, NUMBER_REGEX, RESET_PAGE,
|
||||
} from '../data/constants';
|
||||
import { getAllPossibleQueryParams, updatePathWithQueryParams, windowScrollTo } from '../data/utils';
|
||||
import { trackPasswordResetSuccess, trackResetPasswordPageViewed } from '../tracking/trackers/reset-password';
|
||||
|
||||
const ResetPasswordPage = (props) => {
|
||||
const { formatMessage } = useIntl();
|
||||
@@ -42,6 +43,15 @@ const ResetPasswordPage = (props) => {
|
||||
const { token } = useParams();
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
if (props.status === TOKEN_STATE.VALID) {
|
||||
trackResetPasswordPageViewed();
|
||||
}
|
||||
if (props.status === SUCCESS) {
|
||||
trackPasswordResetSuccess();
|
||||
}
|
||||
}, [props.status]);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.status !== TOKEN_STATE.PENDING && props.status !== PASSWORD_RESET_ERROR) {
|
||||
setErrorCode(props.status);
|
||||
@@ -139,7 +149,7 @@ const ResetPasswordPage = (props) => {
|
||||
}
|
||||
} else if (props.status === PASSWORD_RESET_ERROR) {
|
||||
navigate(updatePathWithQueryParams(RESET_PAGE));
|
||||
} else if (props.status === 'success') {
|
||||
} else if (props.status === SUCCESS) {
|
||||
navigate(updatePathWithQueryParams(LOGIN_PAGE));
|
||||
} else {
|
||||
return (
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { configure, injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { configure, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
fireEvent, render, screen,
|
||||
} from '@testing-library/react';
|
||||
@@ -19,6 +18,11 @@ import ResetPasswordPage from '../ResetPasswordPage';
|
||||
const mockedNavigator = jest.fn();
|
||||
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('react-router-dom', () => ({
|
||||
...(jest.requireActual('react-router-dom')),
|
||||
@@ -26,7 +30,6 @@ jest.mock('react-router-dom', () => ({
|
||||
useParams: jest.fn().mockReturnValue({ token }),
|
||||
}));
|
||||
|
||||
const IntlResetPasswordPage = injectIntl(ResetPasswordPage);
|
||||
const mockStore = configureStore();
|
||||
|
||||
describe('ResetPasswordPage', () => {
|
||||
@@ -95,7 +98,7 @@ describe('ResetPasswordPage', () => {
|
||||
}));
|
||||
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
render(reduxWrapper(<IntlResetPasswordPage {...props} />));
|
||||
render(reduxWrapper(<ResetPasswordPage {...props} />));
|
||||
const newPasswordInput = screen.getByLabelText('New password');
|
||||
const confirmPasswordInput = screen.getByLabelText('Confirm password');
|
||||
|
||||
@@ -120,7 +123,7 @@ describe('ResetPasswordPage', () => {
|
||||
status: TOKEN_STATE.VALID,
|
||||
},
|
||||
});
|
||||
render(reduxWrapper(<IntlResetPasswordPage {...props} />));
|
||||
render(reduxWrapper(<ResetPasswordPage {...props} />));
|
||||
const resetPasswordButton = screen.getByRole('button', { name: /Reset password/i, id: 'submit-new-password' });
|
||||
fireEvent.click(resetPasswordButton);
|
||||
|
||||
@@ -144,7 +147,7 @@ describe('ResetPasswordPage', () => {
|
||||
status: TOKEN_STATE.VALID,
|
||||
},
|
||||
});
|
||||
render(reduxWrapper(<IntlResetPasswordPage {...props} />));
|
||||
render(reduxWrapper(<ResetPasswordPage {...props} />));
|
||||
const confirmPasswordInput = screen.getByLabelText('Confirm password');
|
||||
fireEvent.change(confirmPasswordInput, { target: { value: 'password-mismatch' } });
|
||||
|
||||
@@ -163,7 +166,7 @@ describe('ResetPasswordPage', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const { container } = render(reduxWrapper(<IntlResetPasswordPage {...props} />));
|
||||
const { container } = render(reduxWrapper(<ResetPasswordPage {...props} />));
|
||||
|
||||
const alertElements = container.querySelectorAll('.alert-danger');
|
||||
const rateLimitError = alertElements[0].textContent;
|
||||
@@ -179,7 +182,7 @@ describe('ResetPasswordPage', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const { container } = render(reduxWrapper(<IntlResetPasswordPage {...props} />));
|
||||
const { container } = render(reduxWrapper(<ResetPasswordPage {...props} />));
|
||||
const alertElements = container.querySelectorAll('.alert-danger');
|
||||
const internalServerError = alertElements[0].textContent;
|
||||
expect(internalServerError).toBe(validationMessage);
|
||||
@@ -188,7 +191,7 @@ describe('ResetPasswordPage', () => {
|
||||
// ******** miscellaneous tests ********
|
||||
|
||||
it('should call validation on password field when blur event fires', () => {
|
||||
const resetPasswordPage = render(reduxWrapper(<IntlResetPasswordPage {...props} />));
|
||||
const resetPasswordPage = render(reduxWrapper(<ResetPasswordPage {...props} />));
|
||||
const expectedText = 'Password criteria has not been metPassword must contain at least 8 characters, at least one letter, and at least one number';
|
||||
const newPasswordInput = resetPasswordPage.container.querySelector('input#newPassword');
|
||||
newPasswordInput.value = 'test-password';
|
||||
@@ -207,7 +210,7 @@ describe('ResetPasswordPage', () => {
|
||||
TOKEN_STATE.PENDING,
|
||||
};
|
||||
|
||||
render(reduxWrapper(<IntlResetPasswordPage {...props} />));
|
||||
render(reduxWrapper(<ResetPasswordPage {...props} />));
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith(validateToken(token));
|
||||
});
|
||||
@@ -216,19 +219,19 @@ describe('ResetPasswordPage', () => {
|
||||
status:
|
||||
PASSWORD_RESET_ERROR,
|
||||
};
|
||||
render(reduxWrapper(<IntlResetPasswordPage {...props} />));
|
||||
render(reduxWrapper(<ResetPasswordPage {...props} />));
|
||||
expect(mockedNavigator).toHaveBeenCalledWith(RESET_PAGE);
|
||||
});
|
||||
it('should redirect the user to root url of the application ', async () => {
|
||||
props = {
|
||||
status: SUCCESS,
|
||||
};
|
||||
render(reduxWrapper(<IntlResetPasswordPage {...props} />));
|
||||
render(reduxWrapper(<ResetPasswordPage {...props} />));
|
||||
expect(mockedNavigator).toHaveBeenCalledWith(LOGIN_PAGE);
|
||||
});
|
||||
|
||||
it('shows spinner during token validation', () => {
|
||||
render(reduxWrapper(<IntlResetPasswordPage {...props} />));
|
||||
render(reduxWrapper(<ResetPasswordPage {...props} />));
|
||||
const spinnerElement = document.getElementsByClassName('div.spinner-header');
|
||||
|
||||
expect(spinnerElement).toBeTruthy();
|
||||
@@ -237,7 +240,7 @@ describe('ResetPasswordPage', () => {
|
||||
// ******** redirection tests ********
|
||||
|
||||
it('by clicking on sign in tab should redirect onto login page', async () => {
|
||||
const { getByText } = render(reduxWrapper(<IntlResetPasswordPage {...props} />));
|
||||
const { getByText } = render(reduxWrapper(<ResetPasswordPage {...props} />));
|
||||
|
||||
const signInTab = getByText('Sign in');
|
||||
|
||||
|
||||
@@ -2,19 +2,19 @@
|
||||
.layout {
|
||||
display: flex;
|
||||
|
||||
@include media-breakpoint-down('lg') {
|
||||
@media (--pgn-size-breakpoint-max-width-lg) {
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up('xl') {
|
||||
@media (--pgn-size-breakpoint-min-width-xl) {
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
@include media-breakpoint-up('xl') {
|
||||
@media (--pgn-size-breakpoint-min-width-xl) {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 50vw;
|
||||
@@ -47,7 +47,7 @@
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
|
||||
@include media-breakpoint-down('xl') {
|
||||
@media (--pgn-size-breakpoint-max-width-xl) {
|
||||
font-size: 3.75rem;
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 700;
|
||||
|
||||
@include media-breakpoint-down('xl') {
|
||||
@media (-pgn-size-breakpoint-max-width-xl) {
|
||||
font-size: 1.375rem;
|
||||
line-height: 1.75rem;
|
||||
}
|
||||
@@ -72,7 +72,7 @@
|
||||
}
|
||||
|
||||
.large-screen-left-container {
|
||||
@include media-breakpoint-down('xl') {
|
||||
@media (-pgn-size-breakpoint-max-width-xl) {
|
||||
flex: 0 0 25%;
|
||||
max-width: 25%;
|
||||
}
|
||||
@@ -87,43 +87,43 @@
|
||||
height: 0.25rem;
|
||||
background-image: linear-gradient(
|
||||
102.02deg,
|
||||
$brand-700,
|
||||
$brand-700 20%,
|
||||
$brand 20%,
|
||||
var(--pgn-color-brand-700),
|
||||
var(--pgn-color-brand-700) 20%,
|
||||
var(--pgn-color-brand-base) 20%,
|
||||
);
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
@include media-breakpoint-only('md') {
|
||||
@media (--pgn-size-breakpoint-min-width-md) and (--pgn-size-breakpoint-max-width-md) {
|
||||
.medium-screen-top-stripe {
|
||||
display: flex;
|
||||
height: 0.5rem;
|
||||
background-image: linear-gradient(
|
||||
102.02deg,
|
||||
$brand-700,
|
||||
$brand-700 10%,
|
||||
$brand 10%,
|
||||
$brand 90%,
|
||||
$primary-700 90%,
|
||||
$primary-700 100%,
|
||||
var(--pgn-color-brand-700),
|
||||
var(--pgn-color-brand-700) 10%,
|
||||
var(--pgn-color-brand-base) 10%,
|
||||
var(--pgn-color-brand-base) 90%,
|
||||
var(--pgn-color-primary-700) 90%,
|
||||
var(--pgn-color-primary-700) 100%,
|
||||
);
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-only('lg') {
|
||||
@media (--pgn-size-breakpoint-min-width-lg) and (--pgn-size-breakpoint-max-width-lg){
|
||||
.medium-screen-top-stripe {
|
||||
display: flex;
|
||||
height: 0.5rem;
|
||||
background-image: linear-gradient(
|
||||
102.02deg,
|
||||
$brand-700 10%,
|
||||
$brand 10%,
|
||||
$brand 65%,
|
||||
$primary-700 65%,
|
||||
$primary-700 75%,
|
||||
$accent-a 75%,
|
||||
$accent-a 75%
|
||||
var(--pgn-color-brand-700) 10%,
|
||||
var(--pgn-color-brand-base) 10%,
|
||||
var(--pgn-color-brand-base) 65%,
|
||||
var(--pgn-color-primary-700) 65%,
|
||||
var(--pgn-color-primary-700) 75%,
|
||||
var(--pgn-color-accent-a) 75%,
|
||||
var(--pgn-color-accent-a) 75%
|
||||
);
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
@@ -131,20 +131,20 @@
|
||||
|
||||
.extra-large-screen-top-stripe { display: none; }
|
||||
|
||||
@include media-breakpoint-up('xl') {
|
||||
@media (--pgn-size-breakpoint-min-width-xl) {
|
||||
.extra-large-screen-top-stripe {
|
||||
display: flex;
|
||||
height: 0.5rem;
|
||||
background-image: linear-gradient(
|
||||
102.02deg,
|
||||
$brand-700 10%,
|
||||
$brand 10%,
|
||||
$brand 45%,
|
||||
$primary-700 45%,
|
||||
$primary-700 55%,
|
||||
$accent-a 55%,
|
||||
$accent-a 75%,
|
||||
$info-200 75%,
|
||||
var(--pgn-color-brand-700) 10%,
|
||||
var(--pgn-color-brand-base) 10%,
|
||||
var(--pgn-color-brand-base) 45%,
|
||||
var(--pgn-color-primary-700) 45%,
|
||||
var(--pgn-color-primary-700) 55%,
|
||||
var(--pgn-color-accent-a) 55%,
|
||||
var(--pgn-color-accent-a) 75%,
|
||||
var(--pgn-color-info-200) 75%,
|
||||
);
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
@@ -152,24 +152,24 @@
|
||||
|
||||
.large-screen-svg-light,
|
||||
.large-screen-svg-primary {
|
||||
fill: $light-200;
|
||||
fill: var(--pgn-color-light-200);
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.large-screen-svg-primary {
|
||||
fill: $primary-400;
|
||||
fill: var(--pgn-color-primary-400);
|
||||
}
|
||||
|
||||
.medium-screen-svg-light,
|
||||
.medium-screen-svg-primary {
|
||||
fill: $light-200;
|
||||
fill: var(--pgn-color-light-200);
|
||||
overflow: inherit;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.medium-screen-svg-primary {
|
||||
fill: $primary-400;
|
||||
fill: var(--pgn-color-primary-400);
|
||||
}
|
||||
|
||||
[dir=rtl]{
|
||||
@@ -184,20 +184,20 @@
|
||||
.small-yellow-line {
|
||||
width: 80px;
|
||||
height: 0;
|
||||
border: 2px solid $accent-b;
|
||||
border: 2px solid var(--pgn-color-accent-b);
|
||||
transform: rotate(102.02deg);
|
||||
}
|
||||
|
||||
.medium-yellow-line {
|
||||
width: 120px;
|
||||
height: 0;
|
||||
border: 3px solid $accent-b;
|
||||
border: 3px solid var(--pgn-color-accent-b);
|
||||
transform: rotate(102.02deg);
|
||||
}
|
||||
|
||||
.large-yellow-line {
|
||||
width: 240px;
|
||||
height: 0;
|
||||
border: 3px solid $accent-b;
|
||||
border: 3px solid var(--pgn-color-accent-b);
|
||||
transform: rotate(102.02deg);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 700;
|
||||
|
||||
@include media-breakpoint-down('md') {
|
||||
@media (--pgn-size-breakpoint-max-width-md) {
|
||||
line-height: 1.5rem;
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
@@ -64,52 +64,52 @@ $header-height: 104px;
|
||||
}
|
||||
|
||||
&.light {
|
||||
background-color: $white;
|
||||
background-color: var(--pgn-color-white);
|
||||
|
||||
.title {
|
||||
color: $black;
|
||||
color: var(--pgn-color-black);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: $gray-700;
|
||||
color: var(--pgn-color-gray-700);
|
||||
}
|
||||
|
||||
.badge {
|
||||
background-color: $light-500;
|
||||
color: $black;
|
||||
background-color: var(--pgn-color-light-500);
|
||||
color: var(--pgn-color-black);
|
||||
}
|
||||
|
||||
.footer-content {
|
||||
color: $gray-700;
|
||||
color: var(--pgn-color-gray-700);
|
||||
}
|
||||
}
|
||||
|
||||
&.dark {
|
||||
background-color: $primary-500;
|
||||
background-color: var(--pgn-color-primary-500);
|
||||
|
||||
.pgn__card-header-title-md {
|
||||
color: $white;
|
||||
color: var(--pgn-color-white);
|
||||
}
|
||||
|
||||
.pgn__card-header-subtitle-md {
|
||||
color: $light-200;
|
||||
color: var(--pgn-color-light-200);
|
||||
}
|
||||
|
||||
.title {
|
||||
color: $white;
|
||||
color: var(--pgn-color-white);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: $light-200;
|
||||
color: var(--pgn-color-light-200);
|
||||
}
|
||||
|
||||
.badge {
|
||||
background-color: $dark-200;
|
||||
color: $white;
|
||||
background-color: var(--pgn-color-dark-200);
|
||||
color: var(--pgn-color-white);
|
||||
}
|
||||
|
||||
.footer-content {
|
||||
color: $light-200;
|
||||
color: var(--pgn-color-light-200);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ $card-gap: 24px;
|
||||
.recommendations-container__card-list {
|
||||
gap: $card-gap $card-gap;
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
@media (-pgn-size-breakpoint-max-width-sm) {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
@@ -11,15 +11,15 @@ $card-gap: 24px;
|
||||
flex: 0 1 100%;
|
||||
cursor: pointer;
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
@media (--pgn-size-breakpoint-min-width-sm) {
|
||||
flex: 0 1 calc(50% - #{$card-gap - 12});
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
@media (--pgn-size-breakpoint-min-width-md) {
|
||||
flex: 0 1 calc(33.333% - #{$card-gap - 8});
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
@media (--pgn-size-breakpoint-min-width-lg) {
|
||||
flex: 0 1 calc(25% - #{$card-gap - 6});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,21 +23,21 @@
|
||||
}
|
||||
|
||||
.alert-link {
|
||||
color: $primary !important;
|
||||
color: var(--pgn-color-primary-base) !important;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
color: $info-700 !important;
|
||||
color: var(--pgn-color-info-700) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.email-suggestion-alert-warning {
|
||||
color: $info-500 !important;
|
||||
color: var(--pgn-color-info-500) !important;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
color: $info-700 !important;
|
||||
color: var(--pgn-color-info-700) !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
line-height: 24px;
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
color: $primary-700;
|
||||
color: var(--pgn-color-primary-700);
|
||||
}
|
||||
|
||||
.username-suggestion--label {
|
||||
@@ -99,7 +99,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: map-get($grid-breakpoints, "sm")) {
|
||||
@media (--pgn-size-breakpoint-max-width-sm) {
|
||||
.username-scroll-suggested--form-field {
|
||||
width: 15rem;
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ $elevation-level-2-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.main-content {
|
||||
@extend .pt-4;
|
||||
padding-top: calc(var(--pgn-spacing-spacer-base) * 1.5) !important;
|
||||
min-width: 464px !important;
|
||||
}
|
||||
|
||||
@@ -80,15 +80,15 @@ $elevation-level-2-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.15);
|
||||
.alert-link {
|
||||
font-weight: normal;
|
||||
text-decoration: underline;
|
||||
color: $info-300 !important;
|
||||
color: var(--pgn-color-info-300) !important;
|
||||
|
||||
&:hover {
|
||||
color: $info-500 !important;
|
||||
color: var(--pgn-color-info-500) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.form-control {
|
||||
background-color: $white !important;
|
||||
background-color: var(--pgn-color-white) !important;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
height: 2.75rem;
|
||||
@@ -103,11 +103,11 @@ $elevation-level-2-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.15);
|
||||
margin-bottom: 1rem;
|
||||
font-size: 14px;
|
||||
|
||||
background-color: $white;
|
||||
border: 1px solid $primary;
|
||||
background-color: var(--pgn-color-white);
|
||||
border: 1px solid var(--pgn-color-primary-base);
|
||||
width: 224px;
|
||||
height: 36px;
|
||||
color: $primary;
|
||||
color: var(--pgn-color-primary-base);
|
||||
|
||||
.btn-tpa__image-icon{
|
||||
background-color: transparent;
|
||||
@@ -132,8 +132,8 @@ $elevation-level-2-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.btn-tpa__font-container {
|
||||
background-color: $primary;
|
||||
color: $white;
|
||||
background-color: var(--pgn-color-primary-base);
|
||||
color: var(--pgn-color-white);
|
||||
font-size: 11px;
|
||||
|
||||
margin-left: -6px;
|
||||
@@ -143,7 +143,7 @@ $elevation-level-2-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.btn-oa2-facebook {
|
||||
color: $white;
|
||||
color: var(--pgn-color-white);
|
||||
border-color: $facebook-blue;
|
||||
background-color: $facebook-blue;
|
||||
|
||||
@@ -151,12 +151,12 @@ $elevation-level-2-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.15);
|
||||
&:focus {
|
||||
background-color: $facebook-focus-blue;
|
||||
border: 1px solid $facebook-focus-blue;
|
||||
color: $white;
|
||||
color: var(--pgn-color-white);
|
||||
}
|
||||
}
|
||||
|
||||
.btn-oa2-google-oauth2 {
|
||||
color: $white;
|
||||
color: var(--pgn-color-white);
|
||||
border-color: $google-blue;
|
||||
background-color: $google-blue;
|
||||
|
||||
@@ -171,12 +171,12 @@ $elevation-level-2-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.15);
|
||||
&:focus {
|
||||
background-color: $google-focus-blue;
|
||||
border: 1px solid $google-focus-blue;
|
||||
color: $white;
|
||||
color: var(--pgn-color-white);
|
||||
}
|
||||
}
|
||||
|
||||
.btn-oa2-apple-id {
|
||||
color: $white;
|
||||
color: var(--pgn-color-white);
|
||||
border-color: $apple-black;
|
||||
background-color: $apple-black;
|
||||
font-size: 16px;
|
||||
@@ -190,12 +190,12 @@ $elevation-level-2-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.15);
|
||||
&:focus {
|
||||
background-color: $apple-focus-black;
|
||||
border: 1px solid $apple-focus-black;
|
||||
color: $white;
|
||||
color: var(--pgn-color-white);
|
||||
}
|
||||
}
|
||||
|
||||
.btn-oa2-azuread-oauth2 {
|
||||
color: $white;
|
||||
color: var(--pgn-color-white);
|
||||
border-color: $microsoft-black;
|
||||
background-color: $microsoft-black;
|
||||
|
||||
@@ -203,7 +203,7 @@ $elevation-level-2-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.15);
|
||||
&:focus {
|
||||
background-color: $microsoft-focus-black;
|
||||
border: 1px solid $microsoft-focus-black;
|
||||
color: $white;
|
||||
color: var(--pgn-color-white);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,9 +214,8 @@ $elevation-level-2-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.institute-icon {
|
||||
@extend .mr-1;
|
||||
@extend .text-gray;
|
||||
|
||||
margin: calc(var(--pgn-spacing-spacer-base) * 0.25) !important;
|
||||
color: var(--pgn-color-gray-base) !important;
|
||||
display: inline-block;
|
||||
margin-bottom: 0.25rem;
|
||||
height: 18px;
|
||||
@@ -232,7 +231,7 @@ $elevation-level-2-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.invalid-feedback {
|
||||
color: $red;
|
||||
color: var(--pgn-color-red);
|
||||
}
|
||||
|
||||
.full-vertical-height {
|
||||
@@ -290,22 +289,22 @@ select.form-control {
|
||||
|
||||
#password-requirement-left {
|
||||
opacity: 1;
|
||||
@extend .x-small;
|
||||
font-size: var(--pgn-typography-font-size-xs) !important;
|
||||
filter: drop-shadow($elevation-level-2-shadow) drop-shadow($elevation-level-2-shadow) !important;
|
||||
right: 0.2rem !important;
|
||||
.tooltip-inner {
|
||||
background: $white;
|
||||
background: var(--pgn-color-white);
|
||||
display: block;
|
||||
color: $gray-500;
|
||||
color: var(--pgn-color-gray-500);
|
||||
}
|
||||
.arrow::before {
|
||||
border-left-color: $white;
|
||||
border-left-color: var(--pgn-color-white);
|
||||
}
|
||||
}
|
||||
|
||||
#password-requirement-top {
|
||||
@extend .x-small;
|
||||
filter: drop-shadow($elevation-level-2-shadow) drop-shadow($elevation-level-2-shadow) !important;
|
||||
font-size: var(--pgn-typography-font-size-xs) !important;
|
||||
filter: drop-shadow(var(--pgn-elevation-box-shadow-level-2)) drop-shadow(var(--pgn-elevation-box-shadow-level-2)) !important;
|
||||
opacity: 1;
|
||||
width: 90%;
|
||||
bottom: 10px !important;
|
||||
@@ -314,30 +313,30 @@ select.form-control {
|
||||
|
||||
.tooltip-inner {
|
||||
min-width: 464px !important;
|
||||
background: $white;
|
||||
background: var(--pgn-color-white);
|
||||
display: block;
|
||||
color: $gray-500;
|
||||
color: var(--pgn-color-gray-500);
|
||||
}
|
||||
.arrow::before {
|
||||
border-top-color: $white;
|
||||
border-top-color: var(--pgn-color-white);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.yellow-border {
|
||||
border: 2px solid $accent-b;
|
||||
border: 2px solid var(--pgn-color-accent-b);
|
||||
}
|
||||
|
||||
.institutions__heading {
|
||||
color: $primary-700;
|
||||
color: var(--pgn-color-primary-700);
|
||||
}
|
||||
|
||||
.logistration-button {
|
||||
color: $gray-700;
|
||||
color: var(--pgn-color-gray-700);
|
||||
}
|
||||
|
||||
.logistration-button:hover{
|
||||
color: $gray-700;
|
||||
color: var(--pgn-color-gray-700);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@@ -352,7 +351,7 @@ select.form-control {
|
||||
width: 2.3rem;
|
||||
}
|
||||
.has-floating-label {
|
||||
color: $gray-500;
|
||||
color: var(--pgn-color-gray-500);
|
||||
}
|
||||
|
||||
.pgn__form-control-floating-label .pgn__form-control-floating-label-content {
|
||||
@@ -366,7 +365,7 @@ select.form-control {
|
||||
|
||||
.form-group__form-field .form-control:focus ~ .pgn__form-control-floating-label .pgn__form-control-floating-label-content {
|
||||
font-size: 16px;
|
||||
color: $primary-700;
|
||||
color: var(--pgn-color-primary-700);
|
||||
}
|
||||
|
||||
.form-group__form-field .form-control:not([value='']):not(:focus) ~
|
||||
@@ -444,14 +443,14 @@ select.form-control {
|
||||
}
|
||||
|
||||
.table-striped tbody tr:nth-of-type(odd) {
|
||||
background-color: $light-200;
|
||||
background-color: var(--pgn-color-light-200);
|
||||
}
|
||||
|
||||
.institutions--provider-link {
|
||||
font-weight: normal;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5rem;
|
||||
color: $primary-700
|
||||
color: var(--pgn-color-primary-700)
|
||||
}
|
||||
|
||||
.pgn__form-control-decorator-trailing {
|
||||
|
||||
22
src/tracking/trackers/forgotpassword.js
Normal file
22
src/tracking/trackers/forgotpassword.js
Normal 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')();
|
||||
};
|
||||
29
src/tracking/trackers/login.js
Normal file
29
src/tracking/trackers/login.js
Normal 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,
|
||||
{},
|
||||
)();
|
||||
37
src/tracking/trackers/progressive-profiling.js
Normal file
37
src/tracking/trackers/progressive-profiling.js
Normal 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,
|
||||
{},
|
||||
)();
|
||||
22
src/tracking/trackers/register.js
Normal file
22
src/tracking/trackers/register.js
Normal 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')();
|
||||
};
|
||||
14
src/tracking/trackers/reset-password.js
Normal file
14
src/tracking/trackers/reset-password.js
Normal 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, {})();
|
||||
};
|
||||
37
src/tracking/trackers/tests/forgot-password.test.jsx
Normal file
37
src/tracking/trackers/tests/forgot-password.test.jsx
Normal 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 },
|
||||
);
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user