Compare commits
31 Commits
mashal-m/r
...
split-full
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
25909563a4 | ||
|
|
94e823663e | ||
|
|
dc266a613e | ||
|
|
1b5755664c | ||
|
|
02bd8abcd1 | ||
|
|
a6e96f5ed1 | ||
|
|
872aa48675 | ||
|
|
60efe3cbb7 | ||
|
|
167f86c283 | ||
|
|
02d14a6359 | ||
|
|
a6a473ee5c | ||
|
|
8be469680d | ||
|
|
45e84d3f9c | ||
|
|
d6d71587c7 | ||
|
|
6b70692dd4 | ||
|
|
a056f241b5 | ||
|
|
115ce8d7c6 | ||
|
|
6e58c13ef5 | ||
|
|
65e29a021b | ||
|
|
6c91f01226 | ||
|
|
36a9ebef8c | ||
|
|
5c921fb983 | ||
|
|
98699b08ad | ||
|
|
1f3d1d1aee | ||
|
|
fc60d9f7d1 | ||
|
|
ad7099ad38 | ||
|
|
2ea9301c5e | ||
|
|
b9b4492de9 | ||
|
|
d74b5c49d9 | ||
|
|
27545ea4b6 | ||
|
|
db3655c843 |
1
.env
1
.env
@@ -28,6 +28,7 @@ ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN=''
|
||||
ENABLE_POST_REGISTRATION_RECOMMENDATIONS=''
|
||||
MARKETING_EMAILS_OPT_IN=''
|
||||
SHOW_CONFIGURABLE_EDX_FIELDS=''
|
||||
ENABLE_IMAGE_LAYOUT=''
|
||||
# ***** Zendesk related keys *****
|
||||
ZENDESK_KEY=''
|
||||
ZENDESK_LOGO_URL=''
|
||||
|
||||
@@ -19,6 +19,9 @@ REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
|
||||
SEGMENT_KEY=''
|
||||
SITE_NAME='Your Platform Name Here'
|
||||
INFO_EMAIL='info@example.com'
|
||||
# ***** Features *****
|
||||
ENABLE_DYNAMIC_REGISTRATION_FIELDS='true'
|
||||
ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN='true'
|
||||
# ***** Cookies *****
|
||||
SESSION_COOKIE_DOMAIN='localhost'
|
||||
USER_INFO_COOKIE_NAME='edx-user-info'
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
const { createConfig } = require('@edx/frontend-build');
|
||||
const { createConfig } = require('@openedx/frontend-build');
|
||||
|
||||
module.exports = createConfig('eslint', {
|
||||
rules: {
|
||||
// Temporarily update the 'indent', 'template-curly-spacing' and
|
||||
// 'no-multiple-empty-lines' rules since they are causing eslint
|
||||
// to fail for no apparent reason since upgrading
|
||||
// @edx/frontend-build from v3 to v5:
|
||||
// @openedx/frontend-build from v3 to v5:
|
||||
// - TypeError: Cannot read property 'range' of null
|
||||
indent: [
|
||||
'error',
|
||||
|
||||
5
Makefile
5
Makefile
@@ -53,11 +53,12 @@ pull_translations:
|
||||
rm -rf src/i18n/messages
|
||||
mkdir src/i18n/messages
|
||||
cd src/i18n/messages \
|
||||
&& atlas pull --filter=$(transifex_langs) \
|
||||
&& atlas pull $(ATLAS_OPTIONS) \
|
||||
translations/paragon/src/i18n/messages:paragon \
|
||||
translations/frontend-platform/src/i18n/messages:frontend-platform \
|
||||
translations/frontend-app-authn/src/i18n/messages:frontend-app-authn
|
||||
|
||||
$(intl_imports) paragon frontend-app-authn
|
||||
$(intl_imports) paragon frontend-platform frontend-app-authn
|
||||
endif
|
||||
|
||||
# This target is used by Travis.
|
||||
|
||||
@@ -115,6 +115,10 @@ The authentication micro-frontend also requires the following additional variabl
|
||||
- Name of MFE, this will be used by the API to get runtime configurations for the specific micro frontend. For a frontend repo `frontend-app-appName`, use `appName` as APP_ID.
|
||||
- ``authn`` | ``''``
|
||||
|
||||
* - ``ENABLE_IMAGE_LAYOUT``
|
||||
- Enables the image layout feature within the authn. When set to True, this feature allows the inclusion of images in the base container layout. For more details on configuring this feature, please refer to the `Modifying base container <docs/how_tos/modifying_base_container.rst>`_.
|
||||
- ``true`` | ``''`` (empty strings are falsy)
|
||||
|
||||
|
||||
edX-specific Environment Variables
|
||||
==================================
|
||||
|
||||
39
docs/how_tos/modifying_base_container.rst
Normal file
39
docs/how_tos/modifying_base_container.rst
Normal file
@@ -0,0 +1,39 @@
|
||||
========================================
|
||||
Modifying the Base Container in Authn
|
||||
========================================
|
||||
|
||||
The base container in Authn serves as the fundamental layout structure for rendering different components based on configurations. This document outlines the process for modifying the base container to accommodate changes or customize layouts as needed.
|
||||
|
||||
Understanding Base Container Versions
|
||||
--------------------------------------
|
||||
|
||||
The base container supports two main versions:
|
||||
|
||||
- **Default Layout:** The default layout is the standard layout used when specific configurations do not dictate otherwise.
|
||||
.. image:: ../images/default_layout.png
|
||||
- **Image Layout:** The image layout is an alternative layout option that can be enabled based on configurations.
|
||||
.. image:: ../images/image_layout.png
|
||||
|
||||
Enabling the Image Layout
|
||||
---------------------------
|
||||
|
||||
To activate the image layout feature, navigate to your .env file and update the configurations:
|
||||
|
||||
**Update Configuration**
|
||||
|
||||
Locate the ``ENABLE_IMAGE_LAYOUT`` parameter and set its value to ``true``. Additionally, ensure that the Image configuration settings are provided. Your overall configurations should resemble the following:
|
||||
|
||||
|
||||
.. code-block::
|
||||
|
||||
# ***** Image Layout Configuration *****
|
||||
ENABLE_IMAGE_LAYOUT = True # Set to True to enable image layout feature
|
||||
|
||||
# ***** Base Container Images *****
|
||||
BANNER_IMAGE_LARGE='' # Path to the large banner image
|
||||
BANNER_IMAGE_MEDIUM='' # Path to the medium-sized banner image
|
||||
BANNER_IMAGE_SMALL='' # Path to the small banner image
|
||||
BANNER_IMAGE_EXTRA_SMALL='' # Path to the extra-small banner image
|
||||
|
||||
|
||||
This allows for the customization and adaptation of the base container layout according to specific requirements.
|
||||
BIN
docs/images/default_layout.png
Normal file
BIN
docs/images/default_layout.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 252 KiB |
BIN
docs/images/image_layout.png
Normal file
BIN
docs/images/image_layout.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 MiB |
@@ -1,4 +1,4 @@
|
||||
const { createConfig } = require('@edx/frontend-build');
|
||||
const { createConfig } = require('@openedx/frontend-build');
|
||||
|
||||
module.exports = createConfig('jest', {
|
||||
setupFiles: [
|
||||
|
||||
12626
package-lock.json
generated
12626
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
37
package.json
37
package.json
@@ -33,22 +33,23 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
|
||||
"@edx/frontend-platform": "^5.5.4",
|
||||
"@edx/paragon": "20.46.2",
|
||||
"@fortawesome/fontawesome-svg-core": "6.4.2",
|
||||
"@fortawesome/free-brands-svg-icons": "6.4.2",
|
||||
"@fortawesome/free-solid-svg-icons": "6.4.2",
|
||||
"@edx/frontend-platform": "7.0.1",
|
||||
"@edx/openedx-atlas": "^0.6.0",
|
||||
"@fortawesome/fontawesome-svg-core": "6.5.1",
|
||||
"@fortawesome/free-brands-svg-icons": "6.5.1",
|
||||
"@fortawesome/free-solid-svg-icons": "6.5.1",
|
||||
"@fortawesome/react-fontawesome": "0.2.0",
|
||||
"@openedx/paragon": "^22.1.1",
|
||||
"@optimizely/react-sdk": "^2.9.1",
|
||||
"@redux-devtools/extension": "3.2.5",
|
||||
"@redux-devtools/extension": "3.3.0",
|
||||
"@testing-library/react": "^12.1.5",
|
||||
"@testing-library/react-hooks": "^8.0.1",
|
||||
"algoliasearch": "^4.14.3",
|
||||
"algoliasearch-helper": "^3.14.0",
|
||||
"classnames": "2.3.2",
|
||||
"core-js": "3.32.0",
|
||||
"classnames": "2.5.1",
|
||||
"core-js": "3.35.1",
|
||||
"fastest-levenshtein": "1.0.16",
|
||||
"form-urlencoded": "6.1.0",
|
||||
"form-urlencoded": "6.1.4",
|
||||
"prop-types": "15.8.1",
|
||||
"query-string": "7.1.3",
|
||||
"react": "^17.0.2",
|
||||
@@ -57,30 +58,28 @@
|
||||
"react-loading-skeleton": "3.3.1",
|
||||
"react-redux": "7.2.9",
|
||||
"react-responsive": "8.2.0",
|
||||
"react-router": "6.15.0",
|
||||
"react-router-dom": "6.15.0",
|
||||
"react-router": "6.21.3",
|
||||
"react-router-dom": "6.21.3",
|
||||
"react-zendesk": "^0.1.13",
|
||||
"redux": "4.2.0",
|
||||
"redux-logger": "3.0.6",
|
||||
"redux-mock-store": "1.5.4",
|
||||
"redux-saga": "1.2.3",
|
||||
"redux-saga": "1.3.0",
|
||||
"redux-thunk": "2.4.2",
|
||||
"regenerator-runtime": "0.14.0",
|
||||
"regenerator-runtime": "0.14.1",
|
||||
"reselect": "4.1.8",
|
||||
"universal-cookie": "4.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@edx/browserslist-config": "^1.1.1",
|
||||
"@edx/frontend-build": "13.0.8",
|
||||
"@edx/reactifex": "1.1.0",
|
||||
"@wojtekmaj/enzyme-adapter-react-17": "^0.8.0",
|
||||
"babel-plugin-formatjs": "10.5.10",
|
||||
"enzyme": "3.11.0",
|
||||
"eslint-plugin-import": "2.28.0",
|
||||
"@openedx/frontend-build": "13.0.28",
|
||||
"babel-plugin-formatjs": "10.5.13",
|
||||
"eslint-plugin-import": "2.29.1",
|
||||
"glob": "7.2.3",
|
||||
"history": "5.3.0",
|
||||
"husky": "7.0.4",
|
||||
"jest": "29.6.2",
|
||||
"jest": "29.7.0",
|
||||
"react-test-renderer": "^17.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,50 +1,50 @@
|
||||
import React from 'react';
|
||||
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { mount } from 'enzyme';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import { DefaultLargeLayout, DefaultMediumLayout, DefaultSmallLayout } from './index';
|
||||
|
||||
describe('Default Layout tests', () => {
|
||||
it('should display the form passed as a child in SmallScreenLayout', () => {
|
||||
const smallScreen = mount(
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<div>
|
||||
<DefaultSmallLayout />
|
||||
<form>
|
||||
<form aria-label="form">
|
||||
<input type="text" />
|
||||
</form>
|
||||
</div>
|
||||
</IntlProvider>,
|
||||
);
|
||||
expect(smallScreen.find('form').exists()).toEqual(true);
|
||||
expect(screen.getByRole('form')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should display the form passed as a child in MediumScreenLayout', () => {
|
||||
const mediumScreen = mount(
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<div>
|
||||
<DefaultMediumLayout />
|
||||
<form>
|
||||
<form aria-label="form">
|
||||
<input type="text" />
|
||||
</form>
|
||||
</div>
|
||||
</IntlProvider>,
|
||||
);
|
||||
expect(mediumScreen.find('form').exists()).toEqual(true);
|
||||
expect(screen.getByRole('form')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should display the form passed as a child in LargeScreenLayout', () => {
|
||||
const largeScreen = mount(
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<div>
|
||||
<DefaultLargeLayout />
|
||||
<form>
|
||||
<form aria-label="form">
|
||||
<input type="text" />
|
||||
</form>
|
||||
</div>
|
||||
</IntlProvider>,
|
||||
);
|
||||
expect(largeScreen.find('form').exists()).toEqual(true);
|
||||
expect(screen.getByRole('form')).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink, Image } from '@edx/paragon';
|
||||
import { Hyperlink, Image } from '@openedx/paragon';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink, Image } from '@edx/paragon';
|
||||
import { Hyperlink, Image } from '@openedx/paragon';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink, Image } from '@edx/paragon';
|
||||
import { Hyperlink, Image } from '@openedx/paragon';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink, Image } from '@edx/paragon';
|
||||
import { Hyperlink, Image } from '@openedx/paragon';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink, Image } from '@edx/paragon';
|
||||
import { Hyperlink, Image } from '@openedx/paragon';
|
||||
|
||||
import './index.scss';
|
||||
import messages from './messages';
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink, Image } from '@edx/paragon';
|
||||
import { Hyperlink, Image } from '@openedx/paragon';
|
||||
|
||||
import './index.scss';
|
||||
import messages from './messages';
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink, Image } from '@edx/paragon';
|
||||
import { Hyperlink, Image } from '@openedx/paragon';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
|
||||
@@ -2,12 +2,12 @@ import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink, Image } from '@edx/paragon';
|
||||
import { Hyperlink, Image } from '@openedx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const LargeLayout = ({ username }) => {
|
||||
const LargeLayout = ({ fullName }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
return (
|
||||
@@ -20,7 +20,7 @@ const LargeLayout = ({ username }) => {
|
||||
<div className="large-screen-left-container mr-n4.5 large-yellow-line mt-5" />
|
||||
<div>
|
||||
<h1 className="welcome-to-platform data-hj-suppress">
|
||||
{formatMessage(messages['welcome.to.platform'], { siteName: getConfig().SITE_NAME, username })}
|
||||
{formatMessage(messages['welcome.to.platform'], { siteName: getConfig().SITE_NAME, fullName })}
|
||||
</h1>
|
||||
<h2 className="complete-your-profile">
|
||||
{formatMessage(messages['complete.your.profile.1'])}
|
||||
@@ -43,7 +43,7 @@ const LargeLayout = ({ username }) => {
|
||||
};
|
||||
|
||||
LargeLayout.propTypes = {
|
||||
username: PropTypes.string.isRequired,
|
||||
fullName: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default LargeLayout;
|
||||
|
||||
@@ -2,12 +2,12 @@ import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink, Image } from '@edx/paragon';
|
||||
import { Hyperlink, Image } from '@openedx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const MediumLayout = ({ username }) => {
|
||||
const MediumLayout = ({ fullName }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
return (
|
||||
@@ -22,7 +22,7 @@ const MediumLayout = ({ username }) => {
|
||||
<div className="medium-yellow-line mt-5 mr-n2" />
|
||||
<div>
|
||||
<h1 className="h3 data-hj-suppress mw-320">
|
||||
{formatMessage(messages['welcome.to.platform'], { siteName: getConfig().SITE_NAME, username })}
|
||||
{formatMessage(messages['welcome.to.platform'], { siteName: getConfig().SITE_NAME, fullName })}
|
||||
</h1>
|
||||
<h2 className="display-1">
|
||||
{formatMessage(messages['complete.your.profile.1'])}
|
||||
@@ -46,7 +46,7 @@ const MediumLayout = ({ username }) => {
|
||||
};
|
||||
|
||||
MediumLayout.propTypes = {
|
||||
username: PropTypes.string.isRequired,
|
||||
fullName: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default MediumLayout;
|
||||
|
||||
@@ -2,12 +2,12 @@ import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink, Image } from '@edx/paragon';
|
||||
import { Hyperlink, Image } from '@openedx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const SmallLayout = ({ username }) => {
|
||||
const SmallLayout = ({ fullName }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
return (
|
||||
@@ -20,7 +20,7 @@ const SmallLayout = ({ username }) => {
|
||||
<div className="small-yellow-line mt-4.5" />
|
||||
<div>
|
||||
<h1 className="h5 data-hj-suppress">
|
||||
{formatMessage(messages['welcome.to.platform'], { siteName: getConfig().SITE_NAME, username })}
|
||||
{formatMessage(messages['welcome.to.platform'], { siteName: getConfig().SITE_NAME, fullName })}
|
||||
</h1>
|
||||
<h2 className="h1">
|
||||
{formatMessage(messages['complete.your.profile.1'])}
|
||||
@@ -35,7 +35,7 @@ const SmallLayout = ({ username }) => {
|
||||
};
|
||||
|
||||
SmallLayout.propTypes = {
|
||||
username: PropTypes.string.isRequired,
|
||||
fullName: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default SmallLayout;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
const messages = defineMessages({
|
||||
'welcome.to.platform': {
|
||||
id: 'welcome.to.platform',
|
||||
defaultMessage: 'Welcome to {siteName}, {username}!',
|
||||
defaultMessage: 'Welcome to {siteName}, {fullName}!',
|
||||
description: 'Welcome message that appears on progressive profile page',
|
||||
},
|
||||
'complete.your.profile.1': {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { breakpoints } from '@edx/paragon';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { breakpoints } from '@openedx/paragon';
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import MediaQuery from 'react-responsive';
|
||||
@@ -12,8 +13,9 @@ import {
|
||||
import { AuthLargeLayout, AuthMediumLayout, AuthSmallLayout } from './components/welcome-page-layout';
|
||||
import { DEFAULT_LAYOUT, IMAGE_LAYOUT } from './data/constants';
|
||||
|
||||
const BaseContainer = ({ children, showWelcomeBanner, username }) => {
|
||||
const BaseContainer = ({ children, showWelcomeBanner, fullName }) => {
|
||||
const [baseContainerVersion, setBaseContainerVersion] = useState(DEFAULT_LAYOUT);
|
||||
const enableImageLayout = getConfig().ENABLE_IMAGE_LAYOUT;
|
||||
|
||||
useEffect(() => {
|
||||
const initRebrandExperiment = () => {
|
||||
@@ -30,20 +32,20 @@ const BaseContainer = ({ children, showWelcomeBanner, username }) => {
|
||||
initRebrandExperiment();
|
||||
}, []);
|
||||
|
||||
if (baseContainerVersion === IMAGE_LAYOUT) {
|
||||
if (baseContainerVersion === IMAGE_LAYOUT || enableImageLayout) {
|
||||
return (
|
||||
<div className="layout">
|
||||
<MediaQuery maxWidth={breakpoints.extraSmall.maxWidth - 1}>
|
||||
{showWelcomeBanner ? <AuthSmallLayout username={username} /> : <ImageExtraSmallLayout />}
|
||||
{showWelcomeBanner ? <AuthSmallLayout fullName={fullName} /> : <ImageExtraSmallLayout />}
|
||||
</MediaQuery>
|
||||
<MediaQuery minWidth={breakpoints.small.minWidth} maxWidth={breakpoints.small.maxWidth - 1}>
|
||||
{showWelcomeBanner ? <AuthSmallLayout username={username} /> : <ImageSmallLayout />}
|
||||
{showWelcomeBanner ? <AuthSmallLayout fullName={fullName} /> : <ImageSmallLayout />}
|
||||
</MediaQuery>
|
||||
<MediaQuery minWidth={breakpoints.medium.minWidth} maxWidth={breakpoints.large.maxWidth - 1}>
|
||||
{showWelcomeBanner ? <AuthMediumLayout username={username} /> : <ImageMediumLayout />}
|
||||
{showWelcomeBanner ? <AuthMediumLayout fullName={fullName} /> : <ImageMediumLayout />}
|
||||
</MediaQuery>
|
||||
<MediaQuery minWidth={breakpoints.extraLarge.minWidth}>
|
||||
{showWelcomeBanner ? <AuthLargeLayout username={username} /> : <ImageLargeLayout />}
|
||||
{showWelcomeBanner ? <AuthLargeLayout fullName={fullName} /> : <ImageLargeLayout />}
|
||||
</MediaQuery>
|
||||
<div className={classNames('content', { 'align-items-center mt-0': showWelcomeBanner })}>
|
||||
{children}
|
||||
@@ -57,13 +59,13 @@ const BaseContainer = ({ children, showWelcomeBanner, username }) => {
|
||||
<div className="col-md-12 extra-large-screen-top-stripe" />
|
||||
<div className="layout">
|
||||
<MediaQuery maxWidth={breakpoints.small.maxWidth - 1}>
|
||||
{showWelcomeBanner ? <AuthSmallLayout username={username} /> : <DefaultSmallLayout />}
|
||||
{showWelcomeBanner ? <AuthSmallLayout fullName={fullName} /> : <DefaultSmallLayout />}
|
||||
</MediaQuery>
|
||||
<MediaQuery minWidth={breakpoints.medium.minWidth} maxWidth={breakpoints.large.maxWidth - 1}>
|
||||
{showWelcomeBanner ? <AuthMediumLayout username={username} /> : <DefaultMediumLayout />}
|
||||
{showWelcomeBanner ? <AuthMediumLayout fullName={fullName} /> : <DefaultMediumLayout />}
|
||||
</MediaQuery>
|
||||
<MediaQuery minWidth={breakpoints.extraLarge.minWidth}>
|
||||
{showWelcomeBanner ? <AuthLargeLayout username={username} /> : <DefaultLargeLayout />}
|
||||
{showWelcomeBanner ? <AuthLargeLayout fullName={fullName} /> : <DefaultLargeLayout />}
|
||||
</MediaQuery>
|
||||
<div className={classNames('content', { 'align-items-center mt-0': showWelcomeBanner })}>
|
||||
{children}
|
||||
@@ -75,13 +77,13 @@ const BaseContainer = ({ children, showWelcomeBanner, username }) => {
|
||||
|
||||
BaseContainer.defaultProps = {
|
||||
showWelcomeBanner: false,
|
||||
username: null,
|
||||
fullName: null,
|
||||
};
|
||||
|
||||
BaseContainer.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
showWelcomeBanner: PropTypes.bool,
|
||||
username: PropTypes.string,
|
||||
fullName: PropTypes.string,
|
||||
};
|
||||
|
||||
export default BaseContainer;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React from 'react';
|
||||
|
||||
import { mergeConfig } from '@edx/frontend-platform';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { mount } from 'enzyme';
|
||||
import { render } from '@testing-library/react';
|
||||
import { Context as ResponsiveContext } from 'react-responsive';
|
||||
|
||||
import BaseContainer from '../index';
|
||||
@@ -12,16 +13,16 @@ const LargeScreen = {
|
||||
};
|
||||
|
||||
describe('Base component tests', () => {
|
||||
it('should should default layout', () => {
|
||||
const baseContainer = mount(
|
||||
it('should show default layout', () => {
|
||||
const { container } = render(
|
||||
<IntlProvider locale="en">
|
||||
<BaseContainer />
|
||||
</IntlProvider>,
|
||||
LargeScreen,
|
||||
);
|
||||
|
||||
expect(baseContainer.find('.banner__image').exists()).toBeFalsy();
|
||||
expect(baseContainer.find('.large-screen-svg-primary').exists()).toBeTruthy();
|
||||
expect(container.querySelector('.banner__image')).toBeNull();
|
||||
expect(container.querySelector('.large-screen-svg-primary')).toBeDefined();
|
||||
});
|
||||
|
||||
it('[experiment] should show image layout for treatment group', () => {
|
||||
@@ -31,13 +32,28 @@ describe('Base component tests', () => {
|
||||
},
|
||||
};
|
||||
|
||||
const baseContainer = mount(
|
||||
const { container } = render(
|
||||
<IntlProvider locale="en">
|
||||
<BaseContainer />
|
||||
</IntlProvider>,
|
||||
LargeScreen,
|
||||
);
|
||||
|
||||
expect(baseContainer.find('.banner__image').exists()).toBeTruthy();
|
||||
expect(container.querySelector('.banner__image')).toBeDefined();
|
||||
});
|
||||
|
||||
it('renders Image layout when ENABLE_IMAGE_LAYOUT configuration is enabled', () => {
|
||||
mergeConfig({
|
||||
ENABLE_IMAGE_LAYOUT: true,
|
||||
});
|
||||
|
||||
const { container } = render(
|
||||
<IntlProvider locale="en">
|
||||
<BaseContainer showWelcomeBanner={false} />
|
||||
</IntlProvider>,
|
||||
LargeScreen,
|
||||
);
|
||||
|
||||
expect(container.querySelector('.banner__image')).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,12 +2,12 @@ import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import {
|
||||
Button, Form,
|
||||
Icon,
|
||||
} from '@edx/paragon';
|
||||
import { Login } from '@edx/paragon/icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
} from '@openedx/paragon';
|
||||
import { Login } from '@openedx/paragon/icons';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useState } from 'react';
|
||||
|
||||
import {
|
||||
Form, TransitionReplace,
|
||||
} from '@edx/paragon';
|
||||
} from '@openedx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const FormGroup = (props) => {
|
||||
|
||||
@@ -2,8 +2,8 @@ import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Button, Hyperlink, Icon } from '@edx/paragon';
|
||||
import { Institution } from '@edx/paragon/icons';
|
||||
import { Button, Hyperlink, Icon } from '@openedx/paragon';
|
||||
import { Institution } from '@openedx/paragon/icons';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
@@ -4,10 +4,10 @@ import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Form, Icon, IconButton, OverlayTrigger, Tooltip, useToggle,
|
||||
} from '@edx/paragon';
|
||||
} from '@openedx/paragon';
|
||||
import {
|
||||
Check, Remove, Visibility, VisibilityOff,
|
||||
} from '@edx/paragon/icons';
|
||||
} from '@openedx/paragon/icons';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import messages from './messages';
|
||||
@@ -138,7 +138,7 @@ const PasswordField = (props) => {
|
||||
{props.errorMessage !== '' && (
|
||||
<Form.Control.Feedback key="error" className="form-text-size" hasIcon={false} feedback-for={props.name} type="invalid">
|
||||
{props.errorMessage}
|
||||
<span className="sr-only">{formatMessage(messages['password.sr.only.helping.text'])}</span>
|
||||
{props.showScreenReaderText && <span className="sr-only">{formatMessage(messages['password.sr.only.helping.text'])}</span>}
|
||||
</Form.Control.Feedback>
|
||||
)}
|
||||
</Form.Group>
|
||||
@@ -153,6 +153,7 @@ PasswordField.defaultProps = {
|
||||
handleChange: () => {},
|
||||
handleErrorChange: null,
|
||||
showRequirements: true,
|
||||
showScreenReaderText: true,
|
||||
autoComplete: null,
|
||||
};
|
||||
|
||||
@@ -168,6 +169,7 @@ PasswordField.propTypes = {
|
||||
showRequirements: PropTypes.bool,
|
||||
value: PropTypes.string.isRequired,
|
||||
autoComplete: PropTypes.string,
|
||||
showScreenReaderText: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default PasswordField;
|
||||
|
||||
@@ -2,9 +2,9 @@ import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Icon } from '@edx/paragon';
|
||||
import { Login } from '@edx/paragon/icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { Icon } from '@openedx/paragon';
|
||||
import { Login } from '@openedx/paragon/icons';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
@@ -2,17 +2,22 @@ import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Hyperlink, Icon,
|
||||
} from '@openedx/paragon';
|
||||
import { Institution } from '@openedx/paragon/icons';
|
||||
import PropTypes from 'prop-types';
|
||||
import Skeleton from 'react-loading-skeleton';
|
||||
|
||||
import messages from './messages';
|
||||
import {
|
||||
ENTERPRISE_LOGIN_URL, LOGIN_PAGE, PENDING_STATE, REGISTER_PAGE,
|
||||
} from '../data/constants';
|
||||
|
||||
import {
|
||||
RenderInstitutionButton,
|
||||
SocialAuthProviders,
|
||||
} from '../../common-components';
|
||||
import {
|
||||
PENDING_STATE, REGISTER_PAGE,
|
||||
} from '../../data/constants';
|
||||
import messages from '../messages';
|
||||
} from './index';
|
||||
|
||||
/**
|
||||
* This component renders the Single sign-on (SSO) buttons for the providers passed.
|
||||
@@ -20,19 +25,33 @@ import messages from '../messages';
|
||||
const ThirdPartyAuth = (props) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const {
|
||||
providers, secondaryProviders, currentProvider, handleInstitutionLogin, thirdPartyAuthApiStatus,
|
||||
providers,
|
||||
secondaryProviders,
|
||||
currentProvider,
|
||||
handleInstitutionLogin,
|
||||
thirdPartyAuthApiStatus,
|
||||
isLoginPage,
|
||||
} = props;
|
||||
const isInstitutionAuthActive = !!secondaryProviders.length && !currentProvider;
|
||||
const isSocialAuthActive = !!providers.length && !currentProvider;
|
||||
const isEnterpriseLoginDisabled = getConfig().DISABLE_ENTERPRISE_LOGIN;
|
||||
const enterpriseLoginURL = getConfig().LMS_BASE_URL + ENTERPRISE_LOGIN_URL;
|
||||
|
||||
return (
|
||||
<>
|
||||
{((isEnterpriseLoginDisabled && isInstitutionAuthActive) || isSocialAuthActive) && (
|
||||
<div className="mt-4 mb-3 h4">
|
||||
{formatMessage(messages['registration.other.options.heading'])}
|
||||
{isLoginPage
|
||||
? formatMessage(messages['login.other.options.heading'])
|
||||
: formatMessage(messages['registration.other.options.heading'])}
|
||||
</div>
|
||||
)}
|
||||
{(isLoginPage && !isEnterpriseLoginDisabled && isSocialAuthActive) && (
|
||||
<Hyperlink className="btn btn-link btn-sm text-body p-0 mb-4" destination={enterpriseLoginURL}>
|
||||
<Icon src={Institution} className="institute-icon" />
|
||||
{formatMessage(messages['enterprise.login.btn.text'])}
|
||||
</Hyperlink>
|
||||
)}
|
||||
|
||||
{thirdPartyAuthApiStatus === PENDING_STATE ? (
|
||||
<Skeleton className="tpa-skeleton" height={36} count={2} />
|
||||
@@ -41,12 +60,15 @@ const ThirdPartyAuth = (props) => {
|
||||
{(isEnterpriseLoginDisabled && isInstitutionAuthActive) && (
|
||||
<RenderInstitutionButton
|
||||
onSubmitHandler={handleInstitutionLogin}
|
||||
buttonTitle={formatMessage(messages['register.institution.login.button'])}
|
||||
buttonTitle={formatMessage(messages['institution.login.button'])}
|
||||
/>
|
||||
)}
|
||||
{isSocialAuthActive && (
|
||||
<div className="row m-0">
|
||||
<SocialAuthProviders socialAuthProviders={providers} referrer={REGISTER_PAGE} />
|
||||
<SocialAuthProviders
|
||||
socialAuthProviders={providers}
|
||||
referrer={isLoginPage ? LOGIN_PAGE : REGISTER_PAGE}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
@@ -59,7 +81,8 @@ ThirdPartyAuth.defaultProps = {
|
||||
currentProvider: null,
|
||||
providers: [],
|
||||
secondaryProviders: [],
|
||||
thirdPartyAuthApiStatus: 'pending',
|
||||
thirdPartyAuthApiStatus: PENDING_STATE,
|
||||
isLoginPage: false,
|
||||
};
|
||||
|
||||
ThirdPartyAuth.propTypes = {
|
||||
@@ -86,6 +109,7 @@ ThirdPartyAuth.propTypes = {
|
||||
}),
|
||||
),
|
||||
thirdPartyAuthApiStatus: PropTypes.string,
|
||||
isLoginPage: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default ThirdPartyAuth;
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Alert } from '@edx/paragon';
|
||||
import { Alert } from '@openedx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
@@ -112,6 +112,26 @@ const messages = defineMessages({
|
||||
description: 'Select ticket form',
|
||||
defaultMessage: 'Please choose your request type:',
|
||||
},
|
||||
'registration.other.options.heading': {
|
||||
id: 'registration.other.options.heading',
|
||||
defaultMessage: 'Or register with:',
|
||||
description: 'A message that appears above third party auth providers i.e saml, google, facebook etc',
|
||||
},
|
||||
'institution.login.button': {
|
||||
id: 'institution.login.button',
|
||||
defaultMessage: 'Institution/campus credentials',
|
||||
description: 'shows institutions list',
|
||||
},
|
||||
'login.other.options.heading': {
|
||||
id: 'login.other.options.heading',
|
||||
defaultMessage: 'Or sign in with:',
|
||||
description: 'Text that appears above other sign in options like social auth buttons',
|
||||
},
|
||||
'enterprise.login.btn.text': {
|
||||
id: 'enterprise.login.btn.text',
|
||||
defaultMessage: 'Company or school credentials',
|
||||
description: 'Company or school login link text.',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -10,6 +10,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_IMAGE_LAYOUT: process.env.ENABLE_IMAGE_LAYOUT || false,
|
||||
// Links
|
||||
ACTIVATION_EMAIL_SUPPORT_LINK: process.env.ACTIVATION_EMAIL_SUPPORT_LINK || null,
|
||||
AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK: process.env.AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK || null,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { breakpoints } from '@edx/paragon';
|
||||
import { breakpoints } from '@openedx/paragon';
|
||||
|
||||
/**
|
||||
* A react hook used to determine if the current window is mobile or not.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Form, Icon } from '@edx/paragon';
|
||||
import { ExpandMore } from '@edx/paragon/icons';
|
||||
import { Form, Icon } from '@openedx/paragon';
|
||||
import { ExpandMore } from '@openedx/paragon/icons';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const FormFieldRenderer = (props) => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { mount } from 'enzyme';
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
|
||||
import FieldRenderer from '../FieldRenderer';
|
||||
|
||||
@@ -28,13 +28,14 @@ describe('FieldRendererTests', () => {
|
||||
options: [['1997', '1997'], ['1998', '1998']],
|
||||
};
|
||||
|
||||
const fieldRenderer = mount(<FieldRenderer value={value} fieldData={fieldData} onChangeHandler={changeHandler} />);
|
||||
const field = fieldRenderer.find('select#yob-field');
|
||||
field.simulate('change', { target: { value: 1997 } });
|
||||
const { container } = render(<FieldRenderer value={value} fieldData={fieldData} onChangeHandler={changeHandler} />);
|
||||
const input = container.querySelector('select#yob-field');
|
||||
const label = container.querySelector('label');
|
||||
fireEvent.change(input, { target: { value: 1997 } });
|
||||
|
||||
expect(field.type()).toEqual('select');
|
||||
expect(fieldRenderer.find('label').text()).toEqual('Year of Birth');
|
||||
expect(value).toEqual(1997);
|
||||
expect(input.type).toEqual('select-one');
|
||||
expect(label.textContent).toContain(fieldData.label);
|
||||
expect(value).toEqual('1997');
|
||||
});
|
||||
|
||||
it('should return null if no options are provided for select field', () => {
|
||||
@@ -44,8 +45,8 @@ describe('FieldRendererTests', () => {
|
||||
name: 'yob-field',
|
||||
};
|
||||
|
||||
const fieldRenderer = mount(<FieldRenderer fieldData={fieldData} onChangeHandler={() => {}} />);
|
||||
expect(fieldRenderer.html()).toBeNull();
|
||||
const { container } = render(<FieldRenderer fieldData={fieldData} onChangeHandler={() => {}} />);
|
||||
expect(container.innerHTML).toEqual('');
|
||||
});
|
||||
|
||||
it('should render textarea field', () => {
|
||||
@@ -55,12 +56,13 @@ describe('FieldRendererTests', () => {
|
||||
name: 'goals-field',
|
||||
};
|
||||
|
||||
const fieldRenderer = mount(<FieldRenderer value={value} fieldData={fieldData} onChangeHandler={changeHandler} />);
|
||||
const field = fieldRenderer.find('#goals-field').last();
|
||||
field.simulate('change', { target: { value: 'These are my goals.' } });
|
||||
const { container } = render(<FieldRenderer value={value} fieldData={fieldData} onChangeHandler={changeHandler} />);
|
||||
const input = container.querySelector('#goals-field');
|
||||
const label = container.querySelector('label');
|
||||
fireEvent.change(input, { target: { value: 'These are my goals.' } });
|
||||
|
||||
expect(field.type()).toEqual('textarea');
|
||||
expect(fieldRenderer.find('label').text()).toEqual('Why do you want to join this platform?');
|
||||
expect(input.type).toEqual(fieldData.type);
|
||||
expect(label.textContent).toContain('Why do you want to join this platform?');
|
||||
expect(value).toEqual('These are my goals.');
|
||||
});
|
||||
|
||||
@@ -71,12 +73,13 @@ describe('FieldRendererTests', () => {
|
||||
name: 'company-field',
|
||||
};
|
||||
|
||||
const fieldRenderer = mount(<FieldRenderer value={value} fieldData={fieldData} onChangeHandler={changeHandler} />);
|
||||
const field = fieldRenderer.find('#company-field').last();
|
||||
field.simulate('change', { target: { value: 'ABC' } });
|
||||
const { container } = render(<FieldRenderer value={value} fieldData={fieldData} onChangeHandler={changeHandler} />);
|
||||
const input = container.querySelector('input#company-field');
|
||||
const label = container.querySelector('label');
|
||||
fireEvent.change(input, { target: { value: 'ABC' } });
|
||||
|
||||
expect(field.type()).toEqual('input');
|
||||
expect(fieldRenderer.find('label').text()).toEqual('Company');
|
||||
expect(input.type).toEqual(fieldData.type);
|
||||
expect(label.textContent).toContain(fieldData.label);
|
||||
expect(value).toEqual('ABC');
|
||||
});
|
||||
|
||||
@@ -87,12 +90,13 @@ describe('FieldRendererTests', () => {
|
||||
name: 'marketing-emails-opt-in-field',
|
||||
};
|
||||
|
||||
const fieldRenderer = mount(<FieldRenderer value={value} fieldData={fieldData} onChangeHandler={changeHandler} />);
|
||||
const field = fieldRenderer.find('input#marketing-emails-opt-in-field');
|
||||
field.simulate('change', { target: { checked: true, type: 'checkbox' } });
|
||||
const { container } = render(<FieldRenderer value={value} fieldData={fieldData} onChangeHandler={changeHandler} />);
|
||||
const input = container.querySelector('input#marketing-emails-opt-in-field');
|
||||
const label = container.querySelector('label');
|
||||
fireEvent.click(input);
|
||||
|
||||
expect(field.prop('type')).toEqual('checkbox');
|
||||
expect(fieldRenderer.find('label').text()).toEqual(fieldData.label);
|
||||
expect(input.type).toEqual(fieldData.type);
|
||||
expect(label.textContent).toContain(fieldData.label);
|
||||
expect(value).toEqual(true);
|
||||
});
|
||||
|
||||
@@ -101,8 +105,8 @@ describe('FieldRendererTests', () => {
|
||||
type: 'unknown',
|
||||
};
|
||||
|
||||
const fieldRenderer = mount(<FieldRenderer fieldData={fieldData} onChangeHandler={() => {}} />);
|
||||
expect(fieldRenderer.html()).toBeNull();
|
||||
const { container } = render(<FieldRenderer fieldData={fieldData} onChangeHandler={() => {}} />);
|
||||
expect(container.innerHTML).toContain('');
|
||||
});
|
||||
|
||||
it('should run onBlur and onFocus functions for a field if given', () => {
|
||||
@@ -117,7 +121,7 @@ describe('FieldRendererTests', () => {
|
||||
functionValue = `${e.target.name} focussed`;
|
||||
};
|
||||
|
||||
const fieldRenderer = mount(
|
||||
const { container } = render(
|
||||
<FieldRenderer
|
||||
handleFocus={onFocus}
|
||||
handleBlur={onBlur}
|
||||
@@ -126,19 +130,19 @@ describe('FieldRendererTests', () => {
|
||||
onChangeHandler={changeHandler}
|
||||
/>,
|
||||
);
|
||||
const field = fieldRenderer.find('#test-field').last();
|
||||
const input = container.querySelector('#test-field');
|
||||
|
||||
field.simulate('focus');
|
||||
fireEvent.focus(input);
|
||||
expect(functionValue).toEqual('test-field focussed');
|
||||
|
||||
field.simulate('blur');
|
||||
fireEvent.blur(input);
|
||||
expect(functionValue).toEqual('test-field blurred');
|
||||
});
|
||||
|
||||
it('should render error message for required text fields', () => {
|
||||
const fieldData = { type: 'text', label: 'First Name', name: 'first-name-field' };
|
||||
|
||||
const fieldRenderer = mount(
|
||||
const { container } = render(
|
||||
<FieldRenderer
|
||||
isRequired
|
||||
fieldData={fieldData}
|
||||
@@ -147,7 +151,7 @@ describe('FieldRendererTests', () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(fieldRenderer.find('.form-text-size').last().text()).toEqual('Enter your first name');
|
||||
expect(container.querySelector(`#${fieldData.name}-error`).textContent).toEqual('Enter your first name');
|
||||
});
|
||||
|
||||
it('should render error message for required select fields', () => {
|
||||
@@ -155,7 +159,7 @@ describe('FieldRendererTests', () => {
|
||||
type: 'select', label: 'Preference', name: 'preference-field', options: [['a', 'Opt 1'], ['b', 'Opt 2']],
|
||||
};
|
||||
|
||||
const fieldRenderer = mount(
|
||||
const { container } = render(
|
||||
<FieldRenderer
|
||||
isRequired
|
||||
fieldData={fieldData}
|
||||
@@ -164,13 +168,13 @@ describe('FieldRendererTests', () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(fieldRenderer.find('.form-text-size').last().text()).toEqual('Select your preference');
|
||||
expect(container.querySelector(`#${fieldData.name}-error`).textContent).toEqual('Select your preference');
|
||||
});
|
||||
|
||||
it('should render error message for required textarea fields', () => {
|
||||
const fieldData = { type: 'textarea', label: 'Goals', name: 'goals-field' };
|
||||
|
||||
const fieldRenderer = mount(
|
||||
const { container } = render(
|
||||
<FieldRenderer
|
||||
isRequired
|
||||
fieldData={fieldData}
|
||||
@@ -179,13 +183,13 @@ describe('FieldRendererTests', () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(fieldRenderer.find('.form-text-size').last().text()).toEqual('Tell us your goals');
|
||||
expect(container.querySelector(`#${fieldData.name}-error`).textContent).toEqual('Tell us your goals');
|
||||
});
|
||||
|
||||
it('should render error message for required checkbox fields', () => {
|
||||
const fieldData = { type: 'checkbox', label: 'Honor Code', name: 'honor-code-field' };
|
||||
|
||||
const fieldRenderer = mount(
|
||||
const { container } = render(
|
||||
<FieldRenderer
|
||||
isRequired
|
||||
fieldData={fieldData}
|
||||
@@ -194,6 +198,6 @@ describe('FieldRendererTests', () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(fieldRenderer.find('.form-text-size').last().text()).toEqual('You must agree to our Honor Code');
|
||||
expect(container.querySelector(`#${fieldData.name}-error`).textContent).toEqual('You must agree to our Honor Code');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,8 +2,8 @@ import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Alert } from '@edx/paragon';
|
||||
import { CheckCircle, Error } from '@edx/paragon/icons';
|
||||
import { Alert } from '@openedx/paragon';
|
||||
import { CheckCircle, Error } from '@openedx/paragon/icons';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
@@ -11,8 +11,8 @@ import {
|
||||
StatefulButton,
|
||||
Tab,
|
||||
Tabs,
|
||||
} from '@edx/paragon';
|
||||
import { ChevronLeft } from '@edx/paragon/icons';
|
||||
} from '@openedx/paragon';
|
||||
import { ChevronLeft } from '@openedx/paragon/icons';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { messages as paragonMessages } from '@edx/paragon';
|
||||
import { messages as paragonMessages } from '@openedx/paragon';
|
||||
|
||||
import arMessages from './messages/ar.json';
|
||||
import deMessages from './messages/de.json';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@import "~@edx/brand/paragon/fonts";
|
||||
@import "~@edx/brand/paragon/variables";
|
||||
@import "~@edx/paragon/scss/core/core";
|
||||
@import "~@openedx/paragon/scss/core/core";
|
||||
@import "~@edx/brand/paragon/overrides";
|
||||
|
||||
@import "sass/style";
|
||||
|
||||
@@ -2,36 +2,37 @@ import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Alert } from '@edx/paragon';
|
||||
import { CheckCircle, Error } from '@edx/paragon/icons';
|
||||
import { Alert } from '@openedx/paragon';
|
||||
import { CheckCircle, Error } from '@openedx/paragon/icons';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { ACCOUNT_ACTIVATION_MESSAGE } from './data/constants';
|
||||
import messages from './messages';
|
||||
|
||||
const AccountActivationMessage = (props) => {
|
||||
const AccountActivationMessage = ({ messageType }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { messageType } = props;
|
||||
|
||||
if (!messageType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const variant = messageType === ACCOUNT_ACTIVATION_MESSAGE.ERROR ? 'danger' : messageType;
|
||||
|
||||
const activationOrVerification = getConfig().MARKETING_EMAILS_OPT_IN ? 'confirmation' : 'activation';
|
||||
|
||||
let activationMessage;
|
||||
let heading;
|
||||
|
||||
const activationOrConfirmation = getConfig().MARKETING_EMAILS_OPT_IN ? 'confirmation' : 'activation';
|
||||
const iconMapping = {
|
||||
[ACCOUNT_ACTIVATION_MESSAGE.SUCCESS]: CheckCircle,
|
||||
[ACCOUNT_ACTIVATION_MESSAGE.ERROR]: Error,
|
||||
};
|
||||
|
||||
let activationMessage;
|
||||
let heading;
|
||||
switch (messageType) {
|
||||
case ACCOUNT_ACTIVATION_MESSAGE.SUCCESS: {
|
||||
heading = formatMessage(messages[`account.${activationOrVerification}.success.message.title`]);
|
||||
activationMessage = <span>{formatMessage(messages[`account.${activationOrVerification}.success.message`])}</span>;
|
||||
heading = formatMessage(messages[`account.${activationOrConfirmation}.success.message.title`]);
|
||||
activationMessage = <span>{formatMessage(messages[`account.${activationOrConfirmation}.success.message`])}</span>;
|
||||
break;
|
||||
}
|
||||
case ACCOUNT_ACTIVATION_MESSAGE.INFO: {
|
||||
activationMessage = formatMessage(messages[`account.${activationOrVerification}.info.message`]);
|
||||
activationMessage = formatMessage(messages[`account.${activationOrConfirmation}.info.message`]);
|
||||
break;
|
||||
}
|
||||
case ACCOUNT_ACTIVATION_MESSAGE.ERROR: {
|
||||
@@ -41,7 +42,7 @@ const AccountActivationMessage = (props) => {
|
||||
</Alert.Link>
|
||||
);
|
||||
|
||||
heading = formatMessage(messages[`account.${activationOrVerification}.error.message.title`]);
|
||||
heading = formatMessage(messages[`account.${activationOrConfirmation}.error.message.title`]);
|
||||
activationMessage = (
|
||||
<FormattedMessage
|
||||
id="account.activation.error.message"
|
||||
@@ -59,7 +60,7 @@ const AccountActivationMessage = (props) => {
|
||||
return activationMessage ? (
|
||||
<Alert
|
||||
id="account-activation-message"
|
||||
className="mb-4"
|
||||
className="mb-5"
|
||||
variant={variant}
|
||||
icon={iconMapping[messageType]}
|
||||
>
|
||||
@@ -70,7 +71,11 @@ const AccountActivationMessage = (props) => {
|
||||
};
|
||||
|
||||
AccountActivationMessage.propTypes = {
|
||||
messageType: PropTypes.string.isRequired,
|
||||
messageType: PropTypes.string,
|
||||
};
|
||||
|
||||
AccountActivationMessage.defaultProps = {
|
||||
messageType: null,
|
||||
};
|
||||
|
||||
export default AccountActivationMessage;
|
||||
|
||||
@@ -4,7 +4,7 @@ import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
ActionRow, ModalDialog, useToggle,
|
||||
} from '@edx/paragon';
|
||||
} from '@openedx/paragon';
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthService } from '@edx/frontend-platform/auth';
|
||||
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Alert, Hyperlink } from '@edx/paragon';
|
||||
import { Error } from '@edx/paragon/icons';
|
||||
import { Alert, Hyperlink } from '@openedx/paragon';
|
||||
import { Error } from '@openedx/paragon/icons';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import ChangePasswordPrompt from './ChangePasswordPrompt';
|
||||
@@ -23,22 +23,35 @@ import {
|
||||
TPA_AUTHENTICATION_FAILURE,
|
||||
} from './data/constants';
|
||||
import messages from './messages';
|
||||
import { windowScrollTo } from '../data/utils';
|
||||
|
||||
const LoginFailureMessage = (props) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { context, errorCode } = props.loginError;
|
||||
|
||||
const authService = getAuthService();
|
||||
let errorList;
|
||||
const {
|
||||
context,
|
||||
errorCode,
|
||||
errorCount, // This is used to trigger the useEffect, facilitating the scrolling to the top.
|
||||
} = props;
|
||||
|
||||
useEffect(() => {
|
||||
windowScrollTo({ left: 0, top: 0, behavior: 'smooth' });
|
||||
}, [errorCode, errorCount]);
|
||||
|
||||
if (!errorCode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let resetLink = (
|
||||
<Hyperlink destination="reset" isInline>
|
||||
{formatMessage(messages['login.incorrect.credentials.error.reset.link.text'])}
|
||||
</Hyperlink>
|
||||
);
|
||||
|
||||
let errorMessage;
|
||||
switch (errorCode) {
|
||||
case NON_COMPLIANT_PASSWORD_EXCEPTION: {
|
||||
errorList = (
|
||||
errorMessage = (
|
||||
<>
|
||||
<strong>{formatMessage(messages['non.compliant.password.title'])}</strong>
|
||||
<p>{formatMessage(messages['non.compliant.password.message'])}</p>
|
||||
@@ -47,7 +60,7 @@ const LoginFailureMessage = (props) => {
|
||||
break;
|
||||
}
|
||||
case FORBIDDEN_REQUEST:
|
||||
errorList = <p>{formatMessage(messages['login.rate.limit.reached.message'])}</p>;
|
||||
errorMessage = <p>{formatMessage(messages['login.rate.limit.reached.message'])}</p>;
|
||||
break;
|
||||
case INACTIVE_USER: {
|
||||
const supportLink = (
|
||||
@@ -55,7 +68,7 @@ const LoginFailureMessage = (props) => {
|
||||
{formatMessage(messages['contact.support.link'], { platformName: context.platformName })}
|
||||
</a>
|
||||
);
|
||||
errorList = (
|
||||
errorMessage = (
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="login.inactive.user.error"
|
||||
@@ -64,7 +77,7 @@ const LoginFailureMessage = (props) => {
|
||||
check your spam folders or {supportLink}."
|
||||
values={{
|
||||
lineBreak: <br />,
|
||||
email: <strong className="data-hj-suppress">{props.loginError.email}</strong>,
|
||||
email: <strong className="data-hj-suppress">{context.email}</strong>,
|
||||
supportLink,
|
||||
}}
|
||||
/>
|
||||
@@ -79,7 +92,7 @@ const LoginFailureMessage = (props) => {
|
||||
{formatMessage(messages['tpa.account.link'], { provider: context.provider })}
|
||||
</a>
|
||||
);
|
||||
errorList = (
|
||||
errorMessage = (
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="allowed.domain.login.error"
|
||||
@@ -92,7 +105,7 @@ const LoginFailureMessage = (props) => {
|
||||
break;
|
||||
}
|
||||
case INVALID_FORM:
|
||||
errorList = <p>{formatMessage(messages['login.form.invalid.error.message'])}</p>;
|
||||
errorMessage = <p>{formatMessage(messages['login.form.invalid.error.message'])}</p>;
|
||||
break;
|
||||
case FAILED_LOGIN_ATTEMPT: {
|
||||
resetLink = (
|
||||
@@ -100,7 +113,7 @@ const LoginFailureMessage = (props) => {
|
||||
{formatMessage(messages['login.incorrect.credentials.error.before.account.blocked.text'])}
|
||||
</Hyperlink>
|
||||
);
|
||||
errorList = (
|
||||
errorMessage = (
|
||||
<>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
@@ -124,7 +137,7 @@ const LoginFailureMessage = (props) => {
|
||||
break;
|
||||
}
|
||||
case ACCOUNT_LOCKED_OUT: {
|
||||
errorList = (
|
||||
errorMessage = (
|
||||
<>
|
||||
<p>{formatMessage(messages['account.locked.out.message.1'])}</p>
|
||||
<p>
|
||||
@@ -141,9 +154,9 @@ const LoginFailureMessage = (props) => {
|
||||
}
|
||||
case INCORRECT_EMAIL_PASSWORD:
|
||||
if (context.failureCount <= 1) {
|
||||
errorList = <p>{formatMessage(messages['login.incorrect.credentials.error'])}</p>;
|
||||
errorMessage = <p>{formatMessage(messages['login.incorrect.credentials.error'])}</p>;
|
||||
} else if (context.failureCount === 2) {
|
||||
errorList = (
|
||||
errorMessage = (
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="login.incorrect.credentials.error.with.reset.link"
|
||||
@@ -161,60 +174,56 @@ const LoginFailureMessage = (props) => {
|
||||
}
|
||||
return (
|
||||
<ChangePasswordPrompt
|
||||
redirectUrl={props.loginError.redirectUrl}
|
||||
redirectUrl={context.redirectUrl}
|
||||
variant="nudge"
|
||||
/>
|
||||
);
|
||||
case REQUIRE_PASSWORD_CHANGE:
|
||||
return <ChangePasswordPrompt />;
|
||||
case TPA_AUTHENTICATION_FAILURE:
|
||||
errorList = (
|
||||
<p>{formatMessage(messages['login.tpa.authentication.failure'], {
|
||||
platform_name: getConfig().SITE_NAME,
|
||||
lineBreak: <br />,
|
||||
errorMessage: context.errorMessage,
|
||||
})}
|
||||
errorMessage = (
|
||||
<p>
|
||||
{formatMessage(messages['login.tpa.authentication.failure'], {
|
||||
platform_name: getConfig().SITE_NAME,
|
||||
lineBreak: <br />,
|
||||
errorMessage: context.errorMessage,
|
||||
})}
|
||||
</p>
|
||||
);
|
||||
break;
|
||||
case INTERNAL_SERVER_ERROR:
|
||||
default:
|
||||
errorList = <p>{formatMessage(messages['internal.server.error.message'])}</p>;
|
||||
errorMessage = <p>{formatMessage(messages['internal.server.error.message'])}</p>;
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<Alert id="login-failure-alert" className="mb-5" variant="danger" icon={Error}>
|
||||
<Alert.Heading>{formatMessage(messages['login.failure.header.title'])}</Alert.Heading>
|
||||
{ errorList }
|
||||
{ errorMessage }
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
|
||||
LoginFailureMessage.defaultProps = {
|
||||
loginError: {
|
||||
redirectUrl: null,
|
||||
errorCode: null,
|
||||
errorMessage: null,
|
||||
},
|
||||
context: {},
|
||||
};
|
||||
|
||||
LoginFailureMessage.propTypes = {
|
||||
loginError: PropTypes.shape({
|
||||
context: PropTypes.shape({
|
||||
supportLink: PropTypes.string,
|
||||
platformName: PropTypes.string,
|
||||
tpaHint: PropTypes.string,
|
||||
provider: PropTypes.string,
|
||||
allowedDomain: PropTypes.string,
|
||||
remainingAttempts: PropTypes.number,
|
||||
failureCount: PropTypes.number,
|
||||
errorMessage: PropTypes.string,
|
||||
}),
|
||||
context: PropTypes.shape({
|
||||
supportLink: PropTypes.string,
|
||||
platformName: PropTypes.string,
|
||||
tpaHint: PropTypes.string,
|
||||
provider: PropTypes.string,
|
||||
allowedDomain: PropTypes.string,
|
||||
remainingAttempts: PropTypes.number,
|
||||
failureCount: PropTypes.number,
|
||||
errorMessage: PropTypes.string,
|
||||
email: PropTypes.string,
|
||||
errorCode: PropTypes.string,
|
||||
redirectUrl: PropTypes.string,
|
||||
}),
|
||||
errorCode: PropTypes.string.isRequired,
|
||||
errorCount: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
export default LoginFailureMessage;
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Form, Hyperlink, Icon, StatefulButton,
|
||||
} from '@edx/paragon';
|
||||
import { Institution } from '@edx/paragon/icons';
|
||||
Form, StatefulButton,
|
||||
} from '@openedx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import Skeleton from 'react-loading-skeleton';
|
||||
@@ -15,21 +14,26 @@ import { Link } from 'react-router-dom';
|
||||
|
||||
import AccountActivationMessage from './AccountActivationMessage';
|
||||
import {
|
||||
loginRemovePasswordResetBanner, loginRequest, loginRequestFailure, loginRequestReset, setLoginFormData,
|
||||
backupLoginFormBegin,
|
||||
dismissPasswordResetBanner,
|
||||
loginRequest,
|
||||
} from './data/actions';
|
||||
import { INVALID_FORM, TPA_AUTHENTICATION_FAILURE } from './data/constants';
|
||||
import { loginErrorSelector, loginFormDataSelector, loginRequestSelector } from './data/selectors';
|
||||
import LoginFailureMessage from './LoginFailure';
|
||||
import messages from './messages';
|
||||
import {
|
||||
FormGroup, InstitutionLogistration, PasswordField, RedirectLogistration,
|
||||
RenderInstitutionButton, SocialAuthProviders, ThirdPartyAuthAlert,
|
||||
FormGroup,
|
||||
InstitutionLogistration,
|
||||
PasswordField,
|
||||
RedirectLogistration,
|
||||
ThirdPartyAuthAlert,
|
||||
} from '../common-components';
|
||||
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, ENTERPRISE_LOGIN_URL, PENDING_STATE, RESET_PAGE,
|
||||
DEFAULT_STATE, PENDING_STATE, RESET_PAGE,
|
||||
} from '../data/constants';
|
||||
import {
|
||||
getActivationStatus,
|
||||
@@ -37,324 +41,313 @@ import {
|
||||
getTpaHint,
|
||||
getTpaProvider,
|
||||
updatePathWithQueryParams,
|
||||
windowScrollTo,
|
||||
} from '../data/utils';
|
||||
import ResetPasswordSuccess from '../reset-password/ResetPasswordSuccess';
|
||||
|
||||
class LoginPage extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.state = {
|
||||
password: this.props.loginFormData.password,
|
||||
emailOrUsername: this.props.loginFormData.emailOrUsername,
|
||||
errors: {
|
||||
emailOrUsername: this.props.loginFormData.errors.emailOrUsername,
|
||||
password: this.props.loginFormData.errors.password,
|
||||
},
|
||||
isSubmitted: false,
|
||||
};
|
||||
this.queryParams = getAllPossibleQueryParams();
|
||||
this.tpaHint = getTpaHint();
|
||||
}
|
||||
const LoginPage = (props) => {
|
||||
const {
|
||||
backedUpFormData,
|
||||
loginErrorCode,
|
||||
loginErrorContext,
|
||||
loginResult,
|
||||
shouldBackupState,
|
||||
thirdPartyAuthContext: {
|
||||
providers,
|
||||
currentProvider,
|
||||
secondaryProviders,
|
||||
finishAuthUrl,
|
||||
platformName,
|
||||
errorMessage: thirdPartyErrorMessage,
|
||||
},
|
||||
thirdPartyAuthApiStatus,
|
||||
institutionLogin,
|
||||
showResetPasswordSuccessBanner,
|
||||
submitState,
|
||||
// Actions
|
||||
backupFormState,
|
||||
handleInstitutionLogin,
|
||||
getTPADataFromBackend,
|
||||
} = props;
|
||||
const { formatMessage } = useIntl();
|
||||
const activationMsgType = getActivationStatus();
|
||||
const queryParams = useMemo(() => getAllPossibleQueryParams(), []);
|
||||
|
||||
componentDidMount() {
|
||||
const [formFields, setFormFields] = useState({ ...backedUpFormData.formFields });
|
||||
const [errorCode, setErrorCode] = useState({ type: '', count: 0, context: {} });
|
||||
const [errors, setErrors] = useState({ ...backedUpFormData.errors });
|
||||
const tpaHint = getTpaHint();
|
||||
|
||||
useEffect(() => {
|
||||
sendPageEvent('login_and_registration', 'login');
|
||||
const payload = { ...this.queryParams };
|
||||
}, []);
|
||||
|
||||
if (this.tpaHint) {
|
||||
payload.tpa_hint = this.tpaHint;
|
||||
useEffect(() => {
|
||||
const payload = { ...queryParams };
|
||||
if (tpaHint) {
|
||||
payload.tpa_hint = tpaHint;
|
||||
}
|
||||
this.props.getThirdPartyAuthContext(payload);
|
||||
this.props.loginRequestReset();
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
if (nextProps.loginFormData && this.props.loginFormData !== nextProps.loginFormData) {
|
||||
// Ensuring browser's autofill user credentials get filled and their state persists in the redux store.
|
||||
const nextState = {
|
||||
emailOrUsername: nextProps.loginFormData.emailOrUsername || this.state.emailOrUsername,
|
||||
password: nextProps.loginFormData.password || this.state.password,
|
||||
};
|
||||
this.setState({
|
||||
...nextProps.loginFormData,
|
||||
...nextState,
|
||||
getTPADataFromBackend(payload);
|
||||
}, [getTPADataFromBackend, queryParams, tpaHint]);
|
||||
/**
|
||||
* Backup the login form in redux when login page is toggled.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (shouldBackupState) {
|
||||
backupFormState({
|
||||
formFields: { ...formFields },
|
||||
errors: { ...errors },
|
||||
});
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}, [shouldBackupState, formFields, errors, backupFormState]);
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.props.resetPassword) {
|
||||
this.props.loginRemovePasswordResetBanner();
|
||||
useEffect(() => {
|
||||
if (loginErrorCode) {
|
||||
setErrorCode(prevState => ({
|
||||
type: loginErrorCode,
|
||||
count: prevState.count + 1,
|
||||
context: { ...loginErrorContext },
|
||||
}));
|
||||
}
|
||||
}
|
||||
}, [loginErrorCode, loginErrorContext]);
|
||||
|
||||
getEnterPriseLoginURL() {
|
||||
return getConfig().LMS_BASE_URL + ENTERPRISE_LOGIN_URL;
|
||||
}
|
||||
|
||||
handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
if (this.props.resetPassword) {
|
||||
this.props.loginRemovePasswordResetBanner();
|
||||
}
|
||||
this.setState({ isSubmitted: true });
|
||||
const { emailOrUsername, password } = this.state;
|
||||
const emailValidationError = this.validateEmail(emailOrUsername);
|
||||
const passwordValidationError = this.validatePassword(password);
|
||||
|
||||
if (emailValidationError !== '' || passwordValidationError !== '') {
|
||||
this.props.setLoginFormData({
|
||||
errors: {
|
||||
emailOrUsername: emailValidationError,
|
||||
password: passwordValidationError,
|
||||
useEffect(() => {
|
||||
if (thirdPartyErrorMessage) {
|
||||
setErrorCode((prevState) => ({
|
||||
type: TPA_AUTHENTICATION_FAILURE,
|
||||
count: prevState.count + 1,
|
||||
context: {
|
||||
errorMessage: thirdPartyErrorMessage,
|
||||
},
|
||||
});
|
||||
this.props.loginRequestFailure({
|
||||
errorCode: INVALID_FORM,
|
||||
});
|
||||
}));
|
||||
}
|
||||
}, [thirdPartyErrorMessage]);
|
||||
|
||||
const validateFormFields = (payload) => {
|
||||
const { emailOrUsername, password } = payload;
|
||||
const fieldErrors = { ...errors };
|
||||
|
||||
if (emailOrUsername === '') {
|
||||
fieldErrors.emailOrUsername = formatMessage(messages['email.validation.message']);
|
||||
} else if (emailOrUsername.length < 3) {
|
||||
fieldErrors.emailOrUsername = formatMessage(messages['username.or.email.format.validation.less.chars.message']);
|
||||
}
|
||||
if (password === '') {
|
||||
fieldErrors.password = formatMessage(messages['password.validation.message']);
|
||||
}
|
||||
|
||||
return { ...fieldErrors };
|
||||
};
|
||||
|
||||
const handleSubmit = (event) => {
|
||||
event.preventDefault();
|
||||
if (showResetPasswordSuccessBanner) {
|
||||
props.dismissPasswordResetBanner();
|
||||
}
|
||||
|
||||
const formData = { ...formFields };
|
||||
const validationErrors = validateFormFields(formData);
|
||||
if (validationErrors.emailOrUsername || validationErrors.password) {
|
||||
setErrors({ ...validationErrors });
|
||||
setErrorCode(prevState => ({ type: INVALID_FORM, count: prevState.count + 1, context: {} }));
|
||||
return;
|
||||
}
|
||||
|
||||
// add query params to the payload
|
||||
const payload = {
|
||||
email_or_username: emailOrUsername, password, ...this.queryParams,
|
||||
email_or_username: formData.emailOrUsername,
|
||||
password: formData.password,
|
||||
...queryParams,
|
||||
};
|
||||
this.props.loginRequest(payload);
|
||||
props.loginRequest(payload);
|
||||
};
|
||||
|
||||
handleOnFocus = (e) => {
|
||||
const { errors } = this.state;
|
||||
errors[e.target.name] = '';
|
||||
this.props.setLoginFormData({
|
||||
errors,
|
||||
});
|
||||
const handleOnChange = (event) => {
|
||||
const { name, value } = event.target;
|
||||
setFormFields(prevState => ({ ...prevState, [name]: value }));
|
||||
};
|
||||
|
||||
handleOnBlur = (e) => {
|
||||
const payload = {
|
||||
[e.target.name]: e.target.value,
|
||||
};
|
||||
this.props.setLoginFormData(payload);
|
||||
const handleOnFocus = (event) => {
|
||||
const { name } = event.target;
|
||||
setErrors(prevErrors => ({ ...prevErrors, [name]: '' }));
|
||||
};
|
||||
|
||||
handleForgotPasswordLinkClickEvent = () => {
|
||||
const trackForgotPasswordLinkClick = () => {
|
||||
sendTrackEvent('edx.bi.password-reset_form.toggled', { category: 'user-engagement' });
|
||||
};
|
||||
|
||||
validateEmail(email) {
|
||||
const { errors } = this.state;
|
||||
const { provider, skipHintedLogin } = getTpaProvider(tpaHint, providers, secondaryProviders);
|
||||
|
||||
if (email === '') {
|
||||
errors.emailOrUsername = this.props.intl.formatMessage(messages['email.validation.message']);
|
||||
} else if (email.length < 3) {
|
||||
errors.emailOrUsername = this.props.intl.formatMessage(messages['username.or.email.format.validation.less.chars.message']);
|
||||
} else {
|
||||
errors.emailOrUsername = '';
|
||||
if (tpaHint) {
|
||||
if (thirdPartyAuthApiStatus === PENDING_STATE) {
|
||||
return <Skeleton height={36} />;
|
||||
}
|
||||
|
||||
if (skipHintedLogin) {
|
||||
window.location.href = getConfig().LMS_BASE_URL + provider.loginUrl;
|
||||
return null;
|
||||
}
|
||||
|
||||
if (provider) {
|
||||
return <EnterpriseSSO provider={provider} />;
|
||||
}
|
||||
return errors.emailOrUsername;
|
||||
}
|
||||
|
||||
validatePassword(password) {
|
||||
const { errors } = this.state;
|
||||
errors.password = password.length > 0 ? '' : this.props.intl.formatMessage(messages['password.validation.message']);
|
||||
|
||||
return errors.password;
|
||||
}
|
||||
|
||||
renderThirdPartyAuth(providers, secondaryProviders, currentProvider, thirdPartyAuthApiStatus, intl) {
|
||||
const isInstitutionAuthActive = !!secondaryProviders.length && !currentProvider;
|
||||
const isSocialAuthActive = !!providers.length && !currentProvider;
|
||||
const isEnterpriseLoginDisabled = getConfig().DISABLE_ENTERPRISE_LOGIN;
|
||||
|
||||
if (institutionLogin) {
|
||||
return (
|
||||
<>
|
||||
{(isSocialAuthActive || (isEnterpriseLoginDisabled && isInstitutionAuthActive))
|
||||
&& (
|
||||
<div className="mt-4 mb-3 h4">
|
||||
{intl.formatMessage(messages['login.other.options.heading'])}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(!isEnterpriseLoginDisabled && isSocialAuthActive) && (
|
||||
<Hyperlink className="btn btn-link btn-sm text-body p-0 mb-4" destination={this.getEnterPriseLoginURL()}>
|
||||
<Icon src={Institution} className="institute-icon" />
|
||||
{intl.formatMessage(messages['enterprise.login.btn.text'])}
|
||||
</Hyperlink>
|
||||
)}
|
||||
|
||||
{thirdPartyAuthApiStatus === PENDING_STATE ? (
|
||||
<Skeleton className="tpa-skeleton mb-3" height={30} count={2} />
|
||||
) : (
|
||||
<>
|
||||
{(isEnterpriseLoginDisabled && isInstitutionAuthActive) && (
|
||||
<RenderInstitutionButton
|
||||
onSubmitHandler={this.props.handleInstitutionLogin}
|
||||
buttonTitle={intl.formatMessage(messages['institution.login.button'])}
|
||||
/>
|
||||
)}
|
||||
{isSocialAuthActive && (
|
||||
<div className="row m-0">
|
||||
<SocialAuthProviders socialAuthProviders={providers} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
<InstitutionLogistration
|
||||
secondaryProviders={secondaryProviders}
|
||||
headingTitle={formatMessage(messages['institution.login.page.title'])}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderForm(
|
||||
currentProvider,
|
||||
providers,
|
||||
secondaryProviders,
|
||||
thirdPartyAuthContext,
|
||||
thirdPartyAuthApiStatus,
|
||||
submitState,
|
||||
intl,
|
||||
) {
|
||||
const activationMsgType = getActivationStatus();
|
||||
if (this.props.institutionLogin) {
|
||||
return (
|
||||
<InstitutionLogistration
|
||||
secondaryProviders={thirdPartyAuthContext.secondaryProviders}
|
||||
headingTitle={intl.formatMessage(messages['institution.login.page.title'])}
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{formatMessage(messages['login.page.title'], { siteName: getConfig().SITE_NAME })}</title>
|
||||
</Helmet>
|
||||
<RedirectLogistration
|
||||
success={loginResult.success}
|
||||
redirectUrl={loginResult.redirectUrl}
|
||||
finishAuthUrl={finishAuthUrl}
|
||||
/>
|
||||
<div className="mw-xs mt-3 mb-2">
|
||||
<LoginFailureMessage
|
||||
errorCode={errorCode.type}
|
||||
errorCount={errorCode.count}
|
||||
context={errorCode.context}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const tpaAuthenticationError = {};
|
||||
if (thirdPartyAuthContext.errorMessage) {
|
||||
tpaAuthenticationError.context = {
|
||||
errorMessage: thirdPartyAuthContext.errorMessage,
|
||||
};
|
||||
tpaAuthenticationError.errorCode = TPA_AUTHENTICATION_FAILURE;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{intl.formatMessage(messages['login.page.title'],
|
||||
{ siteName: getConfig().SITE_NAME })}
|
||||
</title>
|
||||
</Helmet>
|
||||
<RedirectLogistration
|
||||
success={this.props.loginResult.success}
|
||||
redirectUrl={this.props.loginResult.redirectUrl}
|
||||
finishAuthUrl={thirdPartyAuthContext.finishAuthUrl}
|
||||
<ThirdPartyAuthAlert
|
||||
currentProvider={currentProvider}
|
||||
platformName={platformName}
|
||||
/>
|
||||
<div className="mw-xs mt-3">
|
||||
<ThirdPartyAuthAlert
|
||||
currentProvider={thirdPartyAuthContext.currentProvider}
|
||||
platformName={thirdPartyAuthContext.platformName}
|
||||
<AccountActivationMessage
|
||||
messageType={activationMsgType}
|
||||
/>
|
||||
{showResetPasswordSuccessBanner && <ResetPasswordSuccess />}
|
||||
<Form id="sign-in-form" name="sign-in-form">
|
||||
<FormGroup
|
||||
name="emailOrUsername"
|
||||
value={formFields.emailOrUsername}
|
||||
autoComplete="on"
|
||||
handleChange={handleOnChange}
|
||||
handleFocus={handleOnFocus}
|
||||
errorMessage={errors.emailOrUsername}
|
||||
floatingLabel={formatMessage(messages['login.user.identity.label'])}
|
||||
/>
|
||||
{this.props.loginError ? <LoginFailureMessage loginError={this.props.loginError} /> : null}
|
||||
{thirdPartyAuthContext.errorMessage ? <LoginFailureMessage loginError={tpaAuthenticationError} /> : null}
|
||||
{submitState === DEFAULT_STATE && this.state.isSubmitted ? windowScrollTo({ left: 0, top: 0, behavior: 'smooth' }) : null}
|
||||
{activationMsgType && <AccountActivationMessage messageType={activationMsgType} />}
|
||||
{this.props.resetPassword && !this.props.loginError ? <ResetPasswordSuccess /> : null}
|
||||
<Form name="sign-in-form" id="sign-in-form">
|
||||
<FormGroup
|
||||
name="emailOrUsername"
|
||||
value={this.state.emailOrUsername}
|
||||
autoComplete="on"
|
||||
handleChange={(e) => this.setState({ emailOrUsername: e.target.value, isSubmitted: false })}
|
||||
handleFocus={this.handleOnFocus}
|
||||
handleBlur={this.handleOnBlur}
|
||||
errorMessage={this.state.errors.emailOrUsername}
|
||||
floatingLabel={intl.formatMessage(messages['login.user.identity.label'])}
|
||||
/>
|
||||
<PasswordField
|
||||
name="password"
|
||||
value={this.state.password}
|
||||
autoComplete="off"
|
||||
showRequirements={false}
|
||||
handleChange={(e) => this.setState({ password: e.target.value, isSubmitted: false })}
|
||||
handleFocus={this.handleOnFocus}
|
||||
handleBlur={this.handleOnBlur}
|
||||
errorMessage={this.state.errors.password}
|
||||
floatingLabel={intl.formatMessage(messages['login.password.label'])}
|
||||
/>
|
||||
<StatefulButton
|
||||
name="sign-in"
|
||||
id="sign-in"
|
||||
type="submit"
|
||||
variant="brand"
|
||||
className="login-button-width"
|
||||
state={submitState}
|
||||
labels={{
|
||||
default: intl.formatMessage(messages['sign.in.button']),
|
||||
pending: '',
|
||||
}}
|
||||
onClick={this.handleSubmit}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
/>
|
||||
<Link
|
||||
id="forgot-password"
|
||||
name="forgot-password"
|
||||
className="btn btn-link font-weight-500 text-body"
|
||||
to={updatePathWithQueryParams(RESET_PAGE)}
|
||||
onClick={this.handleForgotPasswordLinkClickEvent}
|
||||
>
|
||||
{intl.formatMessage(messages['forgot.password'])}
|
||||
</Link>
|
||||
{this.renderThirdPartyAuth(providers, secondaryProviders, currentProvider, thirdPartyAuthApiStatus, intl)}
|
||||
</Form>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
<PasswordField
|
||||
name="password"
|
||||
value={formFields.password}
|
||||
autoComplete="off"
|
||||
showScreenReaderText={false}
|
||||
showRequirements={false}
|
||||
handleChange={handleOnChange}
|
||||
handleFocus={handleOnFocus}
|
||||
errorMessage={errors.password}
|
||||
floatingLabel={formatMessage(messages['login.password.label'])}
|
||||
/>
|
||||
<StatefulButton
|
||||
name="sign-in"
|
||||
id="sign-in"
|
||||
type="submit"
|
||||
variant="brand"
|
||||
className="login-button-width"
|
||||
state={submitState}
|
||||
labels={{
|
||||
default: formatMessage(messages['sign.in.button']),
|
||||
pending: '',
|
||||
}}
|
||||
onClick={handleSubmit}
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
/>
|
||||
<Link
|
||||
id="forgot-password"
|
||||
name="forgot-password"
|
||||
className="btn btn-link font-weight-500 text-body"
|
||||
to={updatePathWithQueryParams(RESET_PAGE)}
|
||||
onClick={trackForgotPasswordLinkClick}
|
||||
>
|
||||
{formatMessage(messages['forgot.password'])}
|
||||
</Link>
|
||||
<ThirdPartyAuth
|
||||
currentProvider={currentProvider}
|
||||
providers={providers}
|
||||
secondaryProviders={secondaryProviders}
|
||||
handleInstitutionLogin={handleInstitutionLogin}
|
||||
thirdPartyAuthApiStatus={thirdPartyAuthApiStatus}
|
||||
isLoginPage
|
||||
/>
|
||||
</Form>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
intl, submitState, thirdPartyAuthContext, thirdPartyAuthApiStatus,
|
||||
} = this.props;
|
||||
const { currentProvider, providers, secondaryProviders } = this.props.thirdPartyAuthContext;
|
||||
const mapStateToProps = state => {
|
||||
const loginPageState = state.login;
|
||||
return {
|
||||
backedUpFormData: loginPageState.loginFormData,
|
||||
loginErrorCode: loginPageState.loginErrorCode,
|
||||
loginErrorContext: loginPageState.loginErrorContext,
|
||||
loginResult: loginPageState.loginResult,
|
||||
shouldBackupState: loginPageState.shouldBackupState,
|
||||
showResetPasswordSuccessBanner: loginPageState.showResetPasswordSuccessBanner,
|
||||
submitState: loginPageState.submitState,
|
||||
thirdPartyAuthContext: thirdPartyAuthContextSelector(state),
|
||||
thirdPartyAuthApiStatus: state.commonComponents.thirdPartyAuthApiStatus,
|
||||
};
|
||||
};
|
||||
|
||||
if (this.tpaHint) {
|
||||
if (thirdPartyAuthApiStatus === PENDING_STATE) {
|
||||
return <Skeleton height={36} />;
|
||||
}
|
||||
const { provider, skipHintedLogin } = getTpaProvider(this.tpaHint, providers, secondaryProviders);
|
||||
if (skipHintedLogin) {
|
||||
window.location.href = getConfig().LMS_BASE_URL + provider.loginUrl;
|
||||
return null;
|
||||
}
|
||||
return provider ? (<EnterpriseSSO provider={provider} intl={intl} />) : this.renderForm(
|
||||
currentProvider,
|
||||
providers,
|
||||
secondaryProviders,
|
||||
thirdPartyAuthContext,
|
||||
thirdPartyAuthApiStatus,
|
||||
submitState,
|
||||
intl,
|
||||
);
|
||||
}
|
||||
return this.renderForm(
|
||||
currentProvider,
|
||||
providers,
|
||||
secondaryProviders,
|
||||
thirdPartyAuthContext,
|
||||
thirdPartyAuthApiStatus,
|
||||
submitState,
|
||||
intl,
|
||||
);
|
||||
}
|
||||
}
|
||||
LoginPage.propTypes = {
|
||||
backedUpFormData: PropTypes.shape({
|
||||
formFields: PropTypes.shape({}),
|
||||
errors: PropTypes.shape({}),
|
||||
}),
|
||||
loginErrorCode: PropTypes.string,
|
||||
loginErrorContext: PropTypes.shape({
|
||||
email: PropTypes.string,
|
||||
redirectUrl: PropTypes.string,
|
||||
context: PropTypes.shape({}),
|
||||
}),
|
||||
loginResult: PropTypes.shape({
|
||||
redirectUrl: PropTypes.string,
|
||||
success: PropTypes.bool,
|
||||
}),
|
||||
shouldBackupState: PropTypes.bool,
|
||||
showResetPasswordSuccessBanner: PropTypes.bool,
|
||||
submitState: PropTypes.string,
|
||||
thirdPartyAuthApiStatus: PropTypes.string,
|
||||
institutionLogin: PropTypes.bool.isRequired,
|
||||
thirdPartyAuthContext: PropTypes.shape({
|
||||
currentProvider: PropTypes.string,
|
||||
errorMessage: PropTypes.string,
|
||||
platformName: PropTypes.string,
|
||||
providers: PropTypes.arrayOf(PropTypes.shape({})),
|
||||
secondaryProviders: PropTypes.arrayOf(PropTypes.shape({})),
|
||||
finishAuthUrl: PropTypes.string,
|
||||
}),
|
||||
// Actions
|
||||
backupFormState: PropTypes.func.isRequired,
|
||||
dismissPasswordResetBanner: PropTypes.func.isRequired,
|
||||
loginRequest: PropTypes.func.isRequired,
|
||||
getTPADataFromBackend: PropTypes.func.isRequired,
|
||||
handleInstitutionLogin: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
LoginPage.defaultProps = {
|
||||
loginResult: null,
|
||||
loginError: null,
|
||||
loginFormData: {
|
||||
emailOrUsername: '',
|
||||
password: '',
|
||||
backedUpFormData: {
|
||||
formFields: {
|
||||
emailOrUsername: '', password: '',
|
||||
},
|
||||
errors: {
|
||||
emailOrUsername: '',
|
||||
password: '',
|
||||
emailOrUsername: '', password: '',
|
||||
},
|
||||
},
|
||||
resetPassword: false,
|
||||
loginErrorCode: null,
|
||||
loginErrorContext: {},
|
||||
loginResult: {},
|
||||
shouldBackupState: false,
|
||||
showResetPasswordSuccessBanner: false,
|
||||
submitState: DEFAULT_STATE,
|
||||
thirdPartyAuthApiStatus: 'pending',
|
||||
thirdPartyAuthApiStatus: PENDING_STATE,
|
||||
thirdPartyAuthContext: {
|
||||
currentProvider: null,
|
||||
errorMessage: null,
|
||||
@@ -364,68 +357,12 @@ LoginPage.defaultProps = {
|
||||
},
|
||||
};
|
||||
|
||||
LoginPage.propTypes = {
|
||||
getThirdPartyAuthContext: PropTypes.func.isRequired,
|
||||
intl: PropTypes.shape({
|
||||
formatMessage: PropTypes.func,
|
||||
}).isRequired,
|
||||
loginError: PropTypes.shape({}),
|
||||
loginRequest: PropTypes.func.isRequired,
|
||||
loginRequestFailure: PropTypes.func.isRequired,
|
||||
loginRequestReset: PropTypes.func.isRequired,
|
||||
setLoginFormData: PropTypes.func.isRequired,
|
||||
loginRemovePasswordResetBanner: PropTypes.func.isRequired,
|
||||
loginResult: PropTypes.shape({
|
||||
redirectUrl: PropTypes.string,
|
||||
success: PropTypes.bool,
|
||||
}),
|
||||
loginFormData: PropTypes.shape({
|
||||
emailOrUsername: PropTypes.string,
|
||||
password: PropTypes.string,
|
||||
errors: PropTypes.shape({
|
||||
emailOrUsername: PropTypes.string,
|
||||
password: PropTypes.string,
|
||||
}),
|
||||
}),
|
||||
resetPassword: PropTypes.bool,
|
||||
submitState: PropTypes.string,
|
||||
thirdPartyAuthApiStatus: PropTypes.string,
|
||||
thirdPartyAuthContext: PropTypes.shape({
|
||||
currentProvider: PropTypes.string,
|
||||
errorMessage: PropTypes.string,
|
||||
platformName: PropTypes.string,
|
||||
providers: PropTypes.arrayOf(PropTypes.shape({})),
|
||||
secondaryProviders: PropTypes.arrayOf(PropTypes.shape({})),
|
||||
finishAuthUrl: PropTypes.string,
|
||||
}),
|
||||
institutionLogin: PropTypes.bool.isRequired,
|
||||
handleInstitutionLogin: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const loginResult = loginRequestSelector(state);
|
||||
const thirdPartyAuthContext = thirdPartyAuthContextSelector(state);
|
||||
const loginError = loginErrorSelector(state);
|
||||
const loginFormData = loginFormDataSelector(state);
|
||||
return {
|
||||
submitState: state.login.submitState,
|
||||
thirdPartyAuthApiStatus: state.commonComponents.thirdPartyAuthApiStatus,
|
||||
loginError,
|
||||
loginResult,
|
||||
thirdPartyAuthContext,
|
||||
loginFormData,
|
||||
resetPassword: state.login.resetPassword,
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
{
|
||||
getThirdPartyAuthContext,
|
||||
backupFormState: backupLoginFormBegin,
|
||||
dismissPasswordResetBanner,
|
||||
loginRequest,
|
||||
loginRequestFailure,
|
||||
loginRequestReset,
|
||||
setLoginFormData,
|
||||
loginRemovePasswordResetBanner,
|
||||
getTPADataFromBackend: getThirdPartyAuthContext,
|
||||
},
|
||||
)(injectIntl(LoginPage));
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
import { AsyncActionType } from '../../data/utils';
|
||||
|
||||
export const BACKUP_LOGIN_DATA = new AsyncActionType('LOGIN', 'BACKUP_LOGIN_DATA');
|
||||
export const LOGIN_REQUEST = new AsyncActionType('LOGIN', 'REQUEST');
|
||||
export const LOGIN_PERSIST_FORM_DATA = 'LOGIN_PERSIST_FORM_DATA';
|
||||
export const LOGIN_REMOVE_PASSWORD_RESET_BANNER = 'LOGIN_REMOVE_PASSWORD_RESET_BANNER';
|
||||
export const DISMISS_PASSWORD_RESET_BANNER = 'DISMISS_PASSWORD_RESET_BANNER';
|
||||
|
||||
// Backup login form data
|
||||
export const backupLoginForm = () => ({
|
||||
type: BACKUP_LOGIN_DATA.BASE,
|
||||
});
|
||||
|
||||
export const backupLoginFormBegin = (data) => ({
|
||||
type: BACKUP_LOGIN_DATA.BEGIN,
|
||||
payload: { ...data },
|
||||
});
|
||||
|
||||
// Login
|
||||
export const loginRequest = creds => ({
|
||||
@@ -24,15 +34,6 @@ export const loginRequestFailure = (loginError) => ({
|
||||
payload: { loginError },
|
||||
});
|
||||
|
||||
export const loginRequestReset = () => ({
|
||||
type: LOGIN_REQUEST.RESET,
|
||||
});
|
||||
|
||||
export const setLoginFormData = (formData) => ({
|
||||
type: LOGIN_PERSIST_FORM_DATA,
|
||||
payload: { formData },
|
||||
});
|
||||
|
||||
export const loginRemovePasswordResetBanner = () => ({
|
||||
type: LOGIN_REMOVE_PASSWORD_RESET_BANNER,
|
||||
export const dismissPasswordResetBanner = () => ({
|
||||
type: DISMISS_PASSWORD_RESET_BANNER,
|
||||
});
|
||||
|
||||
@@ -1,64 +1,69 @@
|
||||
import { LOGIN_PERSIST_FORM_DATA, LOGIN_REMOVE_PASSWORD_RESET_BANNER, LOGIN_REQUEST } from './actions';
|
||||
import {
|
||||
BACKUP_LOGIN_DATA,
|
||||
DISMISS_PASSWORD_RESET_BANNER,
|
||||
LOGIN_REQUEST,
|
||||
} from './actions';
|
||||
import { DEFAULT_STATE, PENDING_STATE } from '../../data/constants';
|
||||
import { RESET_PASSWORD } from '../../reset-password';
|
||||
|
||||
export const defaultState = {
|
||||
loginError: null,
|
||||
loginErrorCode: '',
|
||||
loginErrorContext: {},
|
||||
loginResult: {},
|
||||
resetPassword: false,
|
||||
loginFormData: {
|
||||
password: '',
|
||||
emailOrUsername: '',
|
||||
formFields: {
|
||||
emailOrUsername: '', password: '',
|
||||
},
|
||||
errors: {
|
||||
emailOrUsername: '',
|
||||
password: '',
|
||||
emailOrUsername: '', password: '',
|
||||
},
|
||||
},
|
||||
shouldBackupState: false,
|
||||
showResetPasswordSuccessBanner: false,
|
||||
submitState: DEFAULT_STATE,
|
||||
};
|
||||
|
||||
const reducer = (state = defaultState, action = {}) => {
|
||||
switch (action.type) {
|
||||
case BACKUP_LOGIN_DATA.BASE:
|
||||
return {
|
||||
...state,
|
||||
shouldBackupState: true,
|
||||
};
|
||||
case BACKUP_LOGIN_DATA.BEGIN:
|
||||
return {
|
||||
...defaultState,
|
||||
loginFormData: { ...action.payload },
|
||||
};
|
||||
case LOGIN_REQUEST.BEGIN:
|
||||
return {
|
||||
...state,
|
||||
showResetPasswordSuccessBanner: false,
|
||||
submitState: PENDING_STATE,
|
||||
resetPassword: false,
|
||||
};
|
||||
case LOGIN_REQUEST.SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
loginResult: action.payload,
|
||||
};
|
||||
case LOGIN_REQUEST.FAILURE:
|
||||
case LOGIN_REQUEST.FAILURE: {
|
||||
const { email, loginError, redirectUrl } = action.payload;
|
||||
return {
|
||||
...state,
|
||||
loginError: action.payload.loginError,
|
||||
loginErrorCode: loginError.errorCode,
|
||||
loginErrorContext: { ...loginError.context, email, redirectUrl },
|
||||
submitState: DEFAULT_STATE,
|
||||
};
|
||||
case LOGIN_REQUEST.RESET:
|
||||
return {
|
||||
...state,
|
||||
loginError: null,
|
||||
};
|
||||
}
|
||||
case RESET_PASSWORD.SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
resetPassword: true,
|
||||
showResetPasswordSuccessBanner: true,
|
||||
};
|
||||
case LOGIN_PERSIST_FORM_DATA: {
|
||||
const { formData } = action.payload;
|
||||
case DISMISS_PASSWORD_RESET_BANNER: {
|
||||
return {
|
||||
...state,
|
||||
loginFormData: {
|
||||
...state.loginFormData,
|
||||
...formData,
|
||||
},
|
||||
};
|
||||
}
|
||||
case LOGIN_REMOVE_PASSWORD_RESET_BANNER: {
|
||||
return {
|
||||
...state,
|
||||
resetPassword: false,
|
||||
showResetPasswordSuccessBanner: false,
|
||||
};
|
||||
}
|
||||
default:
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
export const storeName = 'login';
|
||||
|
||||
export const loginSelector = state => ({ ...state[storeName] });
|
||||
|
||||
export const loginRequestSelector = createSelector(
|
||||
loginSelector,
|
||||
login => login.loginResult,
|
||||
);
|
||||
|
||||
export const loginErrorSelector = createSelector(
|
||||
loginSelector,
|
||||
login => login.loginError,
|
||||
);
|
||||
|
||||
export const loginFormDataSelector = createSelector(
|
||||
loginSelector,
|
||||
login => login.loginFormData,
|
||||
);
|
||||
@@ -1,57 +1,154 @@
|
||||
import {
|
||||
LOGIN_PERSIST_FORM_DATA, LOGIN_REMOVE_PASSWORD_RESET_BANNER,
|
||||
} from '../actions';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
import { DEFAULT_REDIRECT_URL, DEFAULT_STATE, PENDING_STATE } from '../../../data/constants';
|
||||
import { RESET_PASSWORD } from '../../../reset-password';
|
||||
import { BACKUP_LOGIN_DATA, DISMISS_PASSWORD_RESET_BANNER, LOGIN_REQUEST } from '../actions';
|
||||
import reducer from '../reducers';
|
||||
|
||||
describe('login reducer', () => {
|
||||
it('should set loginFormData', () => {
|
||||
const state = {
|
||||
loginFormData: {
|
||||
password: '',
|
||||
emailOrUsername: '',
|
||||
errors: {
|
||||
emailOrUsername: '',
|
||||
password: '',
|
||||
},
|
||||
const defaultState = {
|
||||
loginErrorCode: '',
|
||||
loginErrorContext: {},
|
||||
loginResult: {},
|
||||
loginFormData: {
|
||||
formFields: {
|
||||
emailOrUsername: '', password: '',
|
||||
},
|
||||
resetPassword: false,
|
||||
};
|
||||
const formData = {
|
||||
password: 'johndoe',
|
||||
emailOrUsername: 'john@gmail.com',
|
||||
};
|
||||
errors: {
|
||||
emailOrUsername: '', password: '',
|
||||
},
|
||||
},
|
||||
shouldBackupState: false,
|
||||
showResetPasswordSuccessBanner: false,
|
||||
submitState: DEFAULT_STATE,
|
||||
};
|
||||
|
||||
it('should update state to show reset password success banner', () => {
|
||||
const action = {
|
||||
type: LOGIN_PERSIST_FORM_DATA,
|
||||
payload: { formData },
|
||||
type: RESET_PASSWORD.SUCCESS,
|
||||
};
|
||||
|
||||
expect(
|
||||
reducer(state, action),
|
||||
reducer(defaultState, action),
|
||||
).toEqual(
|
||||
{
|
||||
loginFormData: {
|
||||
...state.loginFormData,
|
||||
password: 'johndoe',
|
||||
emailOrUsername: 'john@gmail.com',
|
||||
},
|
||||
resetPassword: false,
|
||||
...defaultState,
|
||||
showResetPasswordSuccessBanner: true,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should set resetPassword', () => {
|
||||
const state = {
|
||||
resetPassword: true,
|
||||
};
|
||||
it('should set the flag which keeps the login form data in redux state', () => {
|
||||
const action = {
|
||||
type: LOGIN_REMOVE_PASSWORD_RESET_BANNER,
|
||||
type: BACKUP_LOGIN_DATA.BASE,
|
||||
};
|
||||
|
||||
expect(
|
||||
reducer(state, action),
|
||||
reducer(defaultState, action),
|
||||
).toEqual(
|
||||
{
|
||||
resetPassword: false,
|
||||
...defaultState,
|
||||
shouldBackupState: true,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should backup the login form data', () => {
|
||||
const payload = {
|
||||
formFields: {
|
||||
emailOrUsername: 'test@exmaple.com',
|
||||
password: 'test1',
|
||||
},
|
||||
errors: {
|
||||
emailOrUsername: '', password: '',
|
||||
},
|
||||
};
|
||||
const action = {
|
||||
type: BACKUP_LOGIN_DATA.BEGIN,
|
||||
payload,
|
||||
};
|
||||
|
||||
expect(
|
||||
reducer(defaultState, action),
|
||||
).toEqual(
|
||||
{
|
||||
...defaultState,
|
||||
loginFormData: payload,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should update state to dismiss reset password banner', () => {
|
||||
const action = {
|
||||
type: DISMISS_PASSWORD_RESET_BANNER,
|
||||
};
|
||||
|
||||
expect(
|
||||
reducer(defaultState, action),
|
||||
).toEqual(
|
||||
{
|
||||
...defaultState,
|
||||
showResetPasswordSuccessBanner: false,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should start the login request', () => {
|
||||
const action = {
|
||||
type: LOGIN_REQUEST.BEGIN,
|
||||
};
|
||||
|
||||
expect(reducer(defaultState, action)).toEqual(
|
||||
{
|
||||
...defaultState,
|
||||
showResetPasswordSuccessBanner: false,
|
||||
submitState: PENDING_STATE,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should set redirect url on login success action', () => {
|
||||
const payload = {
|
||||
redirectUrl: `${getConfig().BASE_URL}${DEFAULT_REDIRECT_URL}`,
|
||||
success: true,
|
||||
};
|
||||
const action = {
|
||||
type: LOGIN_REQUEST.SUCCESS,
|
||||
payload,
|
||||
};
|
||||
|
||||
expect(reducer(defaultState, action)).toEqual(
|
||||
{
|
||||
...defaultState,
|
||||
loginResult: payload,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should set the error data on login request failure', () => {
|
||||
const payload = {
|
||||
loginError: {
|
||||
success: false,
|
||||
value: 'Email or password is incorrect.',
|
||||
errorCode: 'incorrect-email-or-password',
|
||||
context: {
|
||||
failureCount: 0,
|
||||
},
|
||||
},
|
||||
email: 'test@example.com',
|
||||
redirectUrl: '',
|
||||
};
|
||||
const action = {
|
||||
type: LOGIN_REQUEST.FAILURE,
|
||||
payload,
|
||||
};
|
||||
|
||||
expect(reducer(defaultState, action)).toEqual(
|
||||
{
|
||||
...defaultState,
|
||||
loginErrorCode: payload.loginError.errorCode,
|
||||
loginErrorContext: { ...payload.loginError.context, email: payload.email, redirectUrl: payload.redirectUrl },
|
||||
submitState: DEFAULT_STATE,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export const storeName = 'login';
|
||||
|
||||
export { default as LoginPage } from './LoginPage';
|
||||
export { default as reducer } from './data/reducers';
|
||||
export { default as saga } from './data/sagas';
|
||||
export { storeName } from './data/selectors';
|
||||
|
||||
@@ -42,11 +42,6 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Choose your institution from the list below',
|
||||
description: 'Heading of the institutions list',
|
||||
},
|
||||
'login.other.options.heading': {
|
||||
id: 'login.other.options.heading',
|
||||
defaultMessage: 'Or sign in with:',
|
||||
description: 'Text that appears above other sign in options like social auth buttons',
|
||||
},
|
||||
'non.compliant.password.title': {
|
||||
id: 'non.compliant.password.title',
|
||||
defaultMessage: 'We recently changed our password requirements',
|
||||
@@ -64,11 +59,6 @@ const messages = defineMessages({
|
||||
defaultMessage: 'To protect your account, it\'s been temporarily locked. Try again in 30 minutes.',
|
||||
description: 'Part of message for when user account has been locked out after multiple failed login attempts',
|
||||
},
|
||||
'enterprise.login.btn.text': {
|
||||
id: 'enterprise.login.btn.text',
|
||||
defaultMessage: 'Company or school credentials',
|
||||
description: 'Company or school login link text.',
|
||||
},
|
||||
'username.or.email.format.validation.less.chars.message': {
|
||||
id: 'username.or.email.format.validation.less.chars.message',
|
||||
defaultMessage: 'Username or email must have at least 3 characters.',
|
||||
|
||||
@@ -2,7 +2,9 @@ import React from 'react';
|
||||
|
||||
import { mergeConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { mount } from 'enzyme';
|
||||
import {
|
||||
render, screen,
|
||||
} from '@testing-library/react';
|
||||
|
||||
import AccountActivationMessage from '../AccountActivationMessage';
|
||||
import { ACCOUNT_ACTIVATION_MESSAGE } from '../data/constants';
|
||||
@@ -17,18 +19,22 @@ describe('AccountActivationMessage', () => {
|
||||
});
|
||||
|
||||
it('should match account already activated message', () => {
|
||||
const accountActivationMessage = mount(
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<IntlAccountActivationMessage messageType={ACCOUNT_ACTIVATION_MESSAGE.INFO} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
const expectedMessage = 'This account has already been activated.';
|
||||
expect(accountActivationMessage.find('#account-activation-message').find('div').first().text()).toEqual(expectedMessage);
|
||||
|
||||
expect(screen.getByText(
|
||||
'',
|
||||
{ selector: '#account-activation-message' },
|
||||
).textContent).toBe(expectedMessage);
|
||||
});
|
||||
|
||||
it('should match account activated success message', () => {
|
||||
const accountActivationMessage = mount(
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<IntlAccountActivationMessage messageType={ACCOUNT_ACTIVATION_MESSAGE.SUCCESS} />
|
||||
</IntlProvider>,
|
||||
@@ -37,11 +43,15 @@ describe('AccountActivationMessage', () => {
|
||||
const expectedMessage = 'Success! You have activated your account.'
|
||||
+ 'You will now receive email updates and alerts from us related to '
|
||||
+ 'the courses you are enrolled in. Sign in to continue.';
|
||||
expect(accountActivationMessage.find('#account-activation-message').first().text()).toEqual(expectedMessage);
|
||||
|
||||
expect(screen.getByText(
|
||||
'',
|
||||
{ selector: '#account-activation-message' },
|
||||
).textContent).toBe(expectedMessage);
|
||||
});
|
||||
|
||||
it('should match account activation error message', () => {
|
||||
const accountActivationMessage = mount(
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<IntlAccountActivationMessage messageType={ACCOUNT_ACTIVATION_MESSAGE.ERROR} />
|
||||
</IntlProvider>,
|
||||
@@ -49,17 +59,22 @@ describe('AccountActivationMessage', () => {
|
||||
|
||||
const expectedMessage = 'Your account could not be activated'
|
||||
+ 'Something went wrong, please contact support to resolve this issue.';
|
||||
expect(accountActivationMessage.find('#account-activation-message').first().text()).toEqual(expectedMessage);
|
||||
|
||||
expect(screen.getByText(
|
||||
'',
|
||||
{ selector: '#account-activation-message' },
|
||||
).textContent).toBe(expectedMessage);
|
||||
});
|
||||
|
||||
it('should not display anything for invalid message type', () => {
|
||||
const accountActivationMessage = mount(
|
||||
const { container } = render(
|
||||
<IntlProvider locale="en">
|
||||
<IntlAccountActivationMessage messageType="invalid-message" />
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
expect(accountActivationMessage).toEqual({});
|
||||
const accountActivationMessage = container.querySelectorAll('#account-activation-message');
|
||||
expect(accountActivationMessage[0]).toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -71,36 +86,45 @@ describe('EmailConfirmationMessage', () => {
|
||||
});
|
||||
|
||||
it('should match email already confirmed message', () => {
|
||||
const accountVerificationMessage = mount(
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<IntlAccountActivationMessage messageType={ACCOUNT_ACTIVATION_MESSAGE.INFO} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
const expectedMessage = 'This email has already been confirmed.';
|
||||
expect(accountVerificationMessage.find('#account-activation-message').find('div').first().text()).toEqual(expectedMessage);
|
||||
|
||||
expect(screen.getByText(
|
||||
'',
|
||||
{ selector: '#account-activation-message' },
|
||||
).textContent).toBe(expectedMessage);
|
||||
});
|
||||
|
||||
it('should match email confirmation success message', () => {
|
||||
const accountVerificationMessage = mount(
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<IntlAccountActivationMessage messageType={ACCOUNT_ACTIVATION_MESSAGE.SUCCESS} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
const expectedMessage = 'Success! You have confirmed your email.Sign in to continue.';
|
||||
expect(accountVerificationMessage.find('#account-activation-message').first().text()).toEqual(expectedMessage);
|
||||
|
||||
expect(screen.getByText(
|
||||
'',
|
||||
{ selector: '#account-activation-message' },
|
||||
).textContent).toBe(expectedMessage);
|
||||
});
|
||||
|
||||
it('should match email confirmation error message', () => {
|
||||
const accountVerificationMessage = mount(
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<IntlAccountActivationMessage messageType={ACCOUNT_ACTIVATION_MESSAGE.ERROR} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
const expectedMessage = 'Your email could not be confirmed'
|
||||
+ 'Something went wrong, please contact support to resolve this issue.';
|
||||
expect(accountVerificationMessage.find('#account-activation-message').first().text()).toEqual(expectedMessage);
|
||||
expect(screen.getByText(
|
||||
'',
|
||||
{ selector: '#account-activation-message' },
|
||||
).textContent).toBe(expectedMessage);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,7 +2,9 @@ import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { mount } from 'enzyme';
|
||||
import {
|
||||
fireEvent, render, screen,
|
||||
} from '@testing-library/react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
|
||||
@@ -39,7 +41,7 @@ describe('ChangePasswordPromptTests', () => {
|
||||
delete window.location;
|
||||
window.location = { href: getConfig().BASE_URL };
|
||||
|
||||
const changePasswordPrompt = mount(
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<MemoryRouter>
|
||||
<IntlChangePasswordPrompt {...props} />
|
||||
@@ -47,7 +49,7 @@ describe('ChangePasswordPromptTests', () => {
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
changePasswordPrompt.find('button#password-security-close').simulate('click');
|
||||
fireEvent.click(screen.getByText('Close'));
|
||||
expect(window.location.href).toBe(dashboardUrl);
|
||||
});
|
||||
|
||||
@@ -56,7 +58,7 @@ describe('ChangePasswordPromptTests', () => {
|
||||
variant: 'block',
|
||||
};
|
||||
|
||||
const changePasswordPrompt = mount(
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<MemoryRouter>
|
||||
<IntlChangePasswordPrompt {...props} />
|
||||
@@ -65,10 +67,12 @@ describe('ChangePasswordPromptTests', () => {
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await changePasswordPrompt.find('div.pgn__modal-backdrop').first().simulate('click');
|
||||
await fireEvent.click(screen.getByText(
|
||||
'',
|
||||
{ selector: '.pgn__modal-backdrop' },
|
||||
));
|
||||
});
|
||||
|
||||
changePasswordPrompt.update();
|
||||
expect(mockedNavigator).toHaveBeenCalledWith(RESET_PAGE);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import React from 'react';
|
||||
|
||||
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { mount } from 'enzyme';
|
||||
import {
|
||||
render, screen,
|
||||
} from '@testing-library/react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
ACCOUNT_LOCKED_OUT,
|
||||
ALLOWED_DOMAIN_LOGIN_ERROR,
|
||||
FAILED_LOGIN_ATTEMPT,
|
||||
FORBIDDEN_REQUEST,
|
||||
@@ -39,12 +42,11 @@ describe('LoginFailureMessage', () => {
|
||||
|
||||
it('should match non compliant password error message', () => {
|
||||
props = {
|
||||
loginError: {
|
||||
errorCode: NON_COMPLIANT_PASSWORD_EXCEPTION,
|
||||
},
|
||||
errorCode: NON_COMPLIANT_PASSWORD_EXCEPTION,
|
||||
failureCount: 0,
|
||||
};
|
||||
|
||||
const loginFailureMessage = mount(
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<IntlLoginFailureMessage {...props} />
|
||||
</IntlProvider>,
|
||||
@@ -55,22 +57,24 @@ describe('LoginFailureMessage', () => {
|
||||
+ 'password-reset message to the email address associated with this account. '
|
||||
+ 'Thank you for helping us keep your data safe.';
|
||||
|
||||
expect(loginFailureMessage.find('#login-failure-alert').first().text()).toEqual(expectedMessage);
|
||||
expect(screen.getByText(
|
||||
'',
|
||||
{ selector: '#login-failure-alert' },
|
||||
).textContent).toBe(expectedMessage);
|
||||
});
|
||||
|
||||
it('should match inactive user error message', () => {
|
||||
props = {
|
||||
loginError: {
|
||||
context: {
|
||||
email: 'text@example.com',
|
||||
errorCode: INACTIVE_USER,
|
||||
context: {
|
||||
platformName: 'openedX',
|
||||
supportLink: 'http://support.openedx.test',
|
||||
},
|
||||
platformName: 'openedX',
|
||||
supportLink: 'http://support.openedx.test',
|
||||
},
|
||||
errorCode: INACTIVE_USER,
|
||||
failureCount: 0,
|
||||
};
|
||||
|
||||
const loginFailureMessage = mount(
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<IntlLoginFailureMessage {...props} />
|
||||
</IntlProvider>,
|
||||
@@ -80,157 +84,196 @@ describe('LoginFailureMessage', () => {
|
||||
+ 'We just sent an activation link to text@example.com. If you do not receive an email, '
|
||||
+ 'check your spam folders or contact openedX support.';
|
||||
|
||||
expect(loginFailureMessage.find('#login-failure-alert').first().text()).toEqual(expectedMessage);
|
||||
expect(loginFailureMessage.find('#login-failure-alert').find('a').props().href).toEqual('http://support.openedx.test');
|
||||
expect(screen.getByText(
|
||||
'',
|
||||
{ selector: '#login-failure-alert' },
|
||||
).textContent).toBe(expectedMessage);
|
||||
|
||||
expect(screen.getByRole('link', { name: 'contact openedX support' }).getAttribute('href')).toBe('http://support.openedx.test');
|
||||
});
|
||||
|
||||
it('test match failed login attempt error', () => {
|
||||
props = {
|
||||
loginError: {
|
||||
context: {
|
||||
email: 'text@example.com',
|
||||
errorCode: FAILED_LOGIN_ATTEMPT,
|
||||
context: {
|
||||
remainingAttempts: 3,
|
||||
allowedFailureAttempts: 6,
|
||||
resetLink: '/reset',
|
||||
},
|
||||
remainingAttempts: 3,
|
||||
allowedFailureAttempts: 6,
|
||||
resetLink: '/reset',
|
||||
},
|
||||
errorCode: FAILED_LOGIN_ATTEMPT,
|
||||
failureCount: 0,
|
||||
};
|
||||
|
||||
const loginFailureMessage = mount(
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<IntlLoginFailureMessage {...props} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
const expectedMessage = 'We couldn\'t sign you in.The username, email or password you entered is incorrect. '
|
||||
+ 'You have 3 more sign in attempts before your account is temporarily locked.If you\'ve forgotten your password, click here to reset it.';
|
||||
|
||||
expect(loginFailureMessage.find('#login-failure-alert').first().text()).toEqual(expectedMessage);
|
||||
expect(screen.getByText(
|
||||
'',
|
||||
{ selector: '#login-failure-alert' },
|
||||
).textContent).toBe(expectedMessage);
|
||||
});
|
||||
|
||||
it('test match failed login error first attempt', () => {
|
||||
props = {
|
||||
loginError: {
|
||||
context: {
|
||||
email: 'text@example.com',
|
||||
errorCode: INCORRECT_EMAIL_PASSWORD,
|
||||
context: {
|
||||
failureCount: 1,
|
||||
resetLink: '/reset',
|
||||
},
|
||||
failureCount: 1,
|
||||
resetLink: '/reset',
|
||||
},
|
||||
errorCode: INCORRECT_EMAIL_PASSWORD,
|
||||
failureCount: 0,
|
||||
};
|
||||
|
||||
const loginFailureMessage = mount(
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<IntlLoginFailureMessage {...props} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
const expectedMessage = 'We couldn\'t sign you in.The username, email, or password you entered is incorrect. Please try again.';
|
||||
|
||||
expect(loginFailureMessage.find('#login-failure-alert').first().text()).toEqual(expectedMessage);
|
||||
expect(screen.getByText(
|
||||
'',
|
||||
{ selector: '#login-failure-alert' },
|
||||
).textContent).toBe(expectedMessage);
|
||||
});
|
||||
|
||||
it('test match user account locked out', () => {
|
||||
props = {
|
||||
errorCode: ACCOUNT_LOCKED_OUT,
|
||||
failureCount: 0,
|
||||
};
|
||||
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<IntlLoginFailureMessage {...props} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
const expectedMessage = 'We couldn\'t sign you in.To protect your account, it\'s been temporarily locked. Try again in 30 minutes.To be on the safe side, you can reset your password before trying again.';
|
||||
expect(screen.getByText(
|
||||
'',
|
||||
{ selector: '#login-failure-alert' },
|
||||
).textContent).toBe(expectedMessage);
|
||||
});
|
||||
|
||||
it('test match failed login error second attempt', () => {
|
||||
props = {
|
||||
loginError: {
|
||||
context: {
|
||||
email: 'text@example.com',
|
||||
errorCode: INCORRECT_EMAIL_PASSWORD,
|
||||
context: {
|
||||
failureCount: 2,
|
||||
resetLink: '/reset',
|
||||
},
|
||||
failureCount: 2,
|
||||
resetLink: '/reset',
|
||||
},
|
||||
errorCode: INCORRECT_EMAIL_PASSWORD,
|
||||
failureCount: 0,
|
||||
};
|
||||
|
||||
const loginFailureMessage = mount(
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<IntlLoginFailureMessage {...props} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
const expectedMessage = 'We couldn\'t sign you in.The username, email, or password you entered is incorrect. Please try again or reset your password.';
|
||||
|
||||
expect(loginFailureMessage.find('#login-failure-alert').first().text()).toEqual(expectedMessage);
|
||||
expect(screen.getByText(
|
||||
'',
|
||||
{ selector: '#login-failure-alert' },
|
||||
).textContent).toBe(expectedMessage);
|
||||
});
|
||||
|
||||
it('should match rate limit error message', () => {
|
||||
props = {
|
||||
loginError: {
|
||||
errorCode: FORBIDDEN_REQUEST,
|
||||
},
|
||||
errorCode: FORBIDDEN_REQUEST,
|
||||
failureCount: 0,
|
||||
};
|
||||
|
||||
const loginFailureMessage = mount(
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<IntlLoginFailureMessage {...props} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
const expectedMessage = 'We couldn\'t sign you in.Too many failed login attempts. Try again later.';
|
||||
expect(loginFailureMessage.find('#login-failure-alert').first().text()).toEqual(expectedMessage);
|
||||
|
||||
expect(screen.getByText(
|
||||
'',
|
||||
{ selector: '#login-failure-alert' },
|
||||
).textContent).toBe(expectedMessage);
|
||||
});
|
||||
|
||||
it('should match internal server error message', () => {
|
||||
props = {
|
||||
loginError: {
|
||||
errorCode: INTERNAL_SERVER_ERROR,
|
||||
},
|
||||
errorCode: INTERNAL_SERVER_ERROR,
|
||||
failureCount: 0,
|
||||
};
|
||||
|
||||
const loginFailureMessage = mount(
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<IntlLoginFailureMessage {...props} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
const expectedMessage = 'We couldn\'t sign you in.An error has occurred. Try refreshing the page, or check your internet connection.';
|
||||
expect(loginFailureMessage.find('#login-failure-alert').first().text()).toEqual(expectedMessage);
|
||||
|
||||
expect(screen.getByText(
|
||||
'',
|
||||
{ selector: '#login-failure-alert' },
|
||||
).textContent).toBe(expectedMessage);
|
||||
});
|
||||
|
||||
it('should match invalid form error message', () => {
|
||||
props = {
|
||||
loginError: {
|
||||
errorCode: INVALID_FORM,
|
||||
},
|
||||
errorCode: INVALID_FORM,
|
||||
failureCount: 0,
|
||||
};
|
||||
|
||||
const loginFailureMessage = mount(
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<IntlLoginFailureMessage {...props} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
const expectedMessage = 'We couldn\'t sign you in.Please fill in the fields below.';
|
||||
expect(loginFailureMessage.find('#login-failure-alert').first().text()).toEqual(expectedMessage);
|
||||
expect(screen.getByText(
|
||||
'',
|
||||
{ selector: '#login-failure-alert' },
|
||||
).textContent).toBe(expectedMessage);
|
||||
});
|
||||
|
||||
it('should match internal server of error message', () => {
|
||||
props = {
|
||||
loginError: {
|
||||
errorCode: 'invalid-error-code',
|
||||
},
|
||||
errorCode: 'invalid-error-code',
|
||||
failureCount: 0,
|
||||
};
|
||||
|
||||
const loginFailureMessage = mount(
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<IntlLoginFailureMessage {...props} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
const expectedMessage = 'We couldn\'t sign you in.An error has occurred. Try refreshing the page, or check your internet connection.';
|
||||
expect(loginFailureMessage.find('#login-failure-alert').first().text()).toEqual(expectedMessage);
|
||||
expect(screen.getByText(
|
||||
'',
|
||||
{ selector: '#login-failure-alert' },
|
||||
).textContent).toBe(expectedMessage);
|
||||
});
|
||||
|
||||
it('should match tpa authentication failed error message', () => {
|
||||
props = {
|
||||
loginError: {
|
||||
errorCode: TPA_AUTHENTICATION_FAILURE,
|
||||
context: {
|
||||
errorMessage: 'An error occurred',
|
||||
},
|
||||
},
|
||||
errorCode: TPA_AUTHENTICATION_FAILURE,
|
||||
failureCount: 0,
|
||||
context: { errorMessage: 'An error occurred' },
|
||||
};
|
||||
|
||||
const loginFailureMessage = mount(
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<IntlLoginFailureMessage {...props} />
|
||||
</IntlProvider>,
|
||||
@@ -238,18 +281,24 @@ describe('LoginFailureMessage', () => {
|
||||
|
||||
const expectedMessageSubstring = 'We are sorry, you are not authorized to access';
|
||||
|
||||
expect(loginFailureMessage.find('#login-failure-alert').first().text()).toContain(expectedMessageSubstring);
|
||||
expect(loginFailureMessage.find('#login-failure-alert').first().text()).toContain('An error occurred');
|
||||
expect(screen.getByText(
|
||||
'',
|
||||
{ selector: '#login-failure-alert' },
|
||||
).textContent).toContain(expectedMessageSubstring);
|
||||
|
||||
expect(screen.getByText(
|
||||
'',
|
||||
{ selector: '#login-failure-alert' },
|
||||
).textContent).toContain('An error occurred');
|
||||
});
|
||||
|
||||
it('should show modal that nudges users to change password', () => {
|
||||
props = {
|
||||
loginError: {
|
||||
errorCode: NUDGE_PASSWORD_CHANGE,
|
||||
},
|
||||
errorCode: NUDGE_PASSWORD_CHANGE,
|
||||
failureCount: 0,
|
||||
};
|
||||
|
||||
const loginFailureMessage = mount(
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<MemoryRouter>
|
||||
<IntlLoginFailureMessage {...props} />
|
||||
@@ -257,21 +306,25 @@ describe('LoginFailureMessage', () => {
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
expect(loginFailureMessage.find('.pgn__modal-title').text()).toEqual('Password security');
|
||||
expect(loginFailureMessage.find('.pgn__modal-body').text()).toEqual(
|
||||
'Our system detected that your password is vulnerable. '
|
||||
+ 'We recommend you change it so that your account stays secure.',
|
||||
);
|
||||
const message = 'Our system detected that your password is vulnerable. '
|
||||
+ 'We recommend you change it so that your account stays secure.';
|
||||
expect(screen.getByText(
|
||||
'Password security',
|
||||
{ selector: '.pgn__modal-title' },
|
||||
).textContent).toEqual('Password security');
|
||||
expect(screen.getByText(
|
||||
'',
|
||||
{ selector: '.pgn__modal-body' },
|
||||
).textContent).toEqual(message);
|
||||
});
|
||||
|
||||
it('should show modal that requires users to change password', () => {
|
||||
props = {
|
||||
loginError: {
|
||||
errorCode: REQUIRE_PASSWORD_CHANGE,
|
||||
},
|
||||
errorCode: REQUIRE_PASSWORD_CHANGE,
|
||||
failureCount: 0,
|
||||
};
|
||||
|
||||
const loginFailureMessage = mount(
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<MemoryRouter>
|
||||
<IntlLoginFailureMessage {...props} />
|
||||
@@ -279,8 +332,14 @@ describe('LoginFailureMessage', () => {
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
expect(loginFailureMessage.find('.pgn__modal-title').text()).toEqual('Password change required');
|
||||
expect(loginFailureMessage.find('.pgn__modal-body').text()).toEqual(
|
||||
expect(screen.getByText(
|
||||
'Password change required',
|
||||
{ selector: '.pgn__modal-title' },
|
||||
).textContent).toEqual('Password change required');
|
||||
expect(screen.getByText(
|
||||
'',
|
||||
{ selector: '.pgn__modal-body' },
|
||||
).textContent).toEqual(
|
||||
'Our system detected that your password is vulnerable. '
|
||||
+ 'Change your password so that your account stays secure.',
|
||||
);
|
||||
@@ -288,18 +347,17 @@ describe('LoginFailureMessage', () => {
|
||||
|
||||
it('should show message if staff user try to login through password', () => {
|
||||
props = {
|
||||
loginError: {
|
||||
context: {
|
||||
email: 'text@example.com',
|
||||
errorCode: ALLOWED_DOMAIN_LOGIN_ERROR,
|
||||
context: {
|
||||
allowedDomain: 'test.com',
|
||||
provider: 'Google',
|
||||
tpaHint: 'google-auth2',
|
||||
},
|
||||
allowedDomain: 'test.com',
|
||||
provider: 'Google',
|
||||
tpaHint: 'google-auth2',
|
||||
},
|
||||
errorCode: ALLOWED_DOMAIN_LOGIN_ERROR,
|
||||
failureCount: 0,
|
||||
};
|
||||
|
||||
const loginFailureMessage = mount(
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<IntlLoginFailureMessage {...props} />
|
||||
</IntlProvider>,
|
||||
@@ -308,7 +366,11 @@ describe('LoginFailureMessage', () => {
|
||||
const errorMessage = "We couldn't sign you in.As test.com user, You must login with your test.com Google account.";
|
||||
const url = 'http://localhost:18000/dashboard/?tpa_hint=google-auth2';
|
||||
|
||||
expect(loginFailureMessage.find('#login-failure-alert').first().text()).toEqual(errorMessage);
|
||||
expect(loginFailureMessage.find('#login-failure-alert').find('a').props().href).toEqual(url);
|
||||
expect(screen.getByText(
|
||||
'',
|
||||
{ selector: '#login-failure-alert' },
|
||||
).textContent).toContain(errorMessage);
|
||||
|
||||
expect(screen.getByRole('link', { name: 'Google account' }).getAttribute('href')).toBe(url);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,19 +2,18 @@ import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { getConfig, mergeConfig } from '@edx/frontend-platform';
|
||||
import { sendPageEvent } from '@edx/frontend-platform/analytics';
|
||||
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { mount } from 'enzyme';
|
||||
import {
|
||||
fireEvent, render, screen, waitFor,
|
||||
} from '@testing-library/react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import renderer from 'react-test-renderer';
|
||||
import configureStore from 'redux-mock-store';
|
||||
|
||||
import { COMPLETE_STATE, LOGIN_PAGE, PENDING_STATE } from '../../data/constants';
|
||||
import {
|
||||
loginRemovePasswordResetBanner, loginRequest, loginRequestFailure, loginRequestReset, setLoginFormData,
|
||||
} from '../data/actions';
|
||||
import { backupLoginFormBegin, dismissPasswordResetBanner, loginRequest } from '../data/actions';
|
||||
import { INTERNAL_SERVER_ERROR } from '../data/constants';
|
||||
import LoginFailureMessage from '../LoginFailure';
|
||||
import LoginPage from '../LoginPage';
|
||||
|
||||
jest.mock('@edx/frontend-platform/analytics', () => ({
|
||||
@@ -25,15 +24,14 @@ jest.mock('@edx/frontend-platform/auth', () => ({
|
||||
getAuthService: jest.fn(),
|
||||
}));
|
||||
|
||||
const IntlLoginFailureMessage = injectIntl(LoginFailureMessage);
|
||||
const IntlLoginPage = injectIntl(LoginPage);
|
||||
const mockStore = configureStore();
|
||||
|
||||
describe('LoginPage', () => {
|
||||
let props = {};
|
||||
let store = {};
|
||||
let loginFormData = {};
|
||||
|
||||
const emptyFieldValidation = { emailOrUsername: 'Enter your username or email', password: 'Enter your password' };
|
||||
const reduxWrapper = children => (
|
||||
<IntlProvider locale="en">
|
||||
<MemoryRouter>
|
||||
@@ -83,81 +81,151 @@ describe('LoginPage', () => {
|
||||
handleInstitutionLogin: jest.fn(),
|
||||
institutionLogin: false,
|
||||
};
|
||||
loginFormData = {
|
||||
emailOrUsername: '',
|
||||
password: '',
|
||||
errors: {
|
||||
emailOrUsername: '',
|
||||
password: '',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// ******** test login form submission ********
|
||||
|
||||
it('should submit form for valid input', () => {
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
const loginPage = mount(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
|
||||
loginPage.find('input#emailOrUsername').simulate('change', { target: { value: 'test@example.com' } });
|
||||
loginPage.find('input#password').simulate('change', { target: { value: 'password' } });
|
||||
loginPage.find('button.btn-brand').simulate('click');
|
||||
render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith(loginRequest({ email_or_username: 'test@example.com', password: 'password' }));
|
||||
fireEvent.change(screen.getByText(
|
||||
'',
|
||||
{ selector: '#emailOrUsername' },
|
||||
), { target: { value: 'test', name: 'emailOrUsername' } });
|
||||
fireEvent.change(screen.getByText(
|
||||
'',
|
||||
{ selector: '#password' },
|
||||
), { target: { value: 'test-password', name: 'password' } });
|
||||
|
||||
fireEvent.click(screen.getByText(
|
||||
'',
|
||||
{ selector: '.btn-brand' },
|
||||
));
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith(loginRequest({ email_or_username: 'test', password: 'test-password' }));
|
||||
});
|
||||
|
||||
it('should not dispatch loginRequest on empty form submission', () => {
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
const loginPage = mount(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
|
||||
loginPage.find('button.btn-brand').simulate('click');
|
||||
fireEvent.click(screen.getByText(
|
||||
'',
|
||||
{ selector: '.btn-brand' },
|
||||
));
|
||||
expect(store.dispatch).not.toHaveBeenCalledWith(loginRequest({}));
|
||||
});
|
||||
|
||||
it('should dismiss reset password banner on form submission', () => {
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
login: {
|
||||
...initialState.login,
|
||||
showResetPasswordSuccessBanner: true,
|
||||
},
|
||||
});
|
||||
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
fireEvent.click(screen.getByText(
|
||||
'',
|
||||
{ selector: '.btn-brand' },
|
||||
));
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith(dismissPasswordResetBanner());
|
||||
});
|
||||
|
||||
// ******** test login form validations ********
|
||||
|
||||
it('should match state on empty form submission', () => {
|
||||
const errorState = { emailOrUsername: 'Enter your username or email', password: 'Enter your password' };
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
|
||||
const loginPage = (mount(reduxWrapper(<IntlLoginPage {...props} />))).find('LoginPage');
|
||||
loginPage.find('button.btn-brand').simulate('click');
|
||||
|
||||
// Check that loginRequestFailure was dispatched and state is updated
|
||||
expect(loginPage.state('errors')).toEqual(errorState);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(loginRequestFailure({ errorCode: 'invalid-form' }));
|
||||
});
|
||||
|
||||
it('should match state for invalid email (less than 3 characters), on form submission', () => {
|
||||
const errorState = { emailOrUsername: 'Username or email must have at least 3 characters.', password: '' };
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
|
||||
const loginPage = (mount(reduxWrapper(<IntlLoginPage {...props} />))).find('LoginPage');
|
||||
render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
|
||||
loginPage.find('input#password').simulate('change', { target: { value: 'test', name: 'password' } });
|
||||
loginPage.find('input#emailOrUsername').simulate('change', { target: { value: 'te', name: 'email' } });
|
||||
loginPage.find('button.btn-brand').simulate('click');
|
||||
fireEvent.change(screen.getByText(
|
||||
'',
|
||||
{ selector: '#password' },
|
||||
), { target: { value: 'test' } });
|
||||
fireEvent.change(screen.getByText(
|
||||
'',
|
||||
{ selector: '#emailOrUsername' },
|
||||
), { target: { value: 'te' } });
|
||||
|
||||
expect(loginPage.state('errors')).toEqual(errorState);
|
||||
fireEvent.click(screen.getByText(
|
||||
'',
|
||||
{ selector: '.btn-brand' },
|
||||
));
|
||||
|
||||
expect(screen.getByText('Username or email must have at least 3 characters.')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should reset field related error messages on onFocus event', () => {
|
||||
const errorState = { emailOrUsername: '', password: '' };
|
||||
it('should show error messages for required fields on empty form submission', () => {
|
||||
const { container } = render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
fireEvent.click(screen.getByText(
|
||||
'',
|
||||
{ selector: '.btn-brand' },
|
||||
));
|
||||
|
||||
expect(container.querySelector('div[feedback-for="emailOrUsername"]').textContent).toEqual(emptyFieldValidation.emailOrUsername);
|
||||
expect(container.querySelector('div[feedback-for="password"]').textContent).toEqual(emptyFieldValidation.password);
|
||||
|
||||
const alertBanner = 'We couldn\'t sign you in.Please fill in the fields below.';
|
||||
expect(container.querySelector('#login-failure-alert').textContent).toEqual(alertBanner);
|
||||
});
|
||||
|
||||
it('should run frontend validations for emailOrUsername field on form submission', () => {
|
||||
const { container } = render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
|
||||
fireEvent.change(screen.getByText(
|
||||
'',
|
||||
{ selector: '#emailOrUsername' },
|
||||
), { target: { value: 'te', name: 'emailOrUsername' } });
|
||||
|
||||
fireEvent.click(screen.getByText(
|
||||
'',
|
||||
{ selector: '.btn-brand' },
|
||||
));
|
||||
|
||||
expect(container.querySelector('div[feedback-for="emailOrUsername"]').textContent).toEqual('Username or email must have at least 3 characters.');
|
||||
});
|
||||
|
||||
// ******** test field focus in functionality ********
|
||||
it('should reset field related error messages on onFocus event', async () => {
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
|
||||
const loginPage = (mount(reduxWrapper(<IntlLoginPage {...props} />))).find('LoginPage');
|
||||
loginPage.find('button.btn-brand').simulate('click');
|
||||
render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
|
||||
loginPage.find('input#emailOrUsername').simulate('focus');
|
||||
loginPage.find('input#password').simulate('focus');
|
||||
expect(loginPage.state('errors')).toEqual(errorState);
|
||||
await act(async () => {
|
||||
// clicking submit button with empty fields to make the errors appear
|
||||
fireEvent.click(screen.getByText(
|
||||
'',
|
||||
{ selector: '.btn-brand' },
|
||||
));
|
||||
|
||||
// focusing the fields to verify that the errors are cleared
|
||||
fireEvent.focus(screen.getByText(
|
||||
'',
|
||||
{ selector: '#password' },
|
||||
));
|
||||
fireEvent.focus(screen.getByText(
|
||||
'',
|
||||
{ selector: '#emailOrUsername' },
|
||||
));
|
||||
});
|
||||
|
||||
// verifying that the errors are cleared
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Enter your username or email')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
// ******** test form buttons and links ********
|
||||
|
||||
it('should match default button state', () => {
|
||||
const loginPage = mount(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(loginPage.find('button[type="submit"] span').first().text()).toEqual('Sign in');
|
||||
render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(screen.getByText('Sign in')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should match pending button state', () => {
|
||||
@@ -169,97 +237,105 @@ describe('LoginPage', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const loginPage = mount(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
const button = loginPage.find('button[type="submit"] span').first();
|
||||
render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
|
||||
expect(button.find('.sr-only').text()).toEqual('pending');
|
||||
expect(screen.getByText(
|
||||
'pending',
|
||||
).textContent).toEqual('pending');
|
||||
});
|
||||
|
||||
it('should show forgot password link', () => {
|
||||
const loginPage = mount(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(loginPage.find('a#forgot-password').text()).toEqual('Forgot password');
|
||||
render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
|
||||
expect(screen.getByText(
|
||||
'Forgot password',
|
||||
{ selector: '#forgot-password' },
|
||||
).textContent).toEqual('Forgot password');
|
||||
});
|
||||
|
||||
it('should show single sign on provider button', () => {
|
||||
mergeConfig({
|
||||
DISABLE_ENTERPRISE_LOGIN: '',
|
||||
});
|
||||
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
commonComponents: {
|
||||
...initialState.commonComponents,
|
||||
thirdPartyAuthContext: {
|
||||
...initialState.commonComponents.thirdPartyAuthContext,
|
||||
providers: [{
|
||||
...ssoProvider,
|
||||
}],
|
||||
providers: [ssoProvider],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const loginPage = mount(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(loginPage.find(`button#${ssoProvider.id}`).length).toEqual(1);
|
||||
render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(screen.getByText(
|
||||
'',
|
||||
{ selector: `#${ssoProvider.id}` },
|
||||
)).toBeDefined();
|
||||
|
||||
mergeConfig({
|
||||
DISABLE_ENTERPRISE_LOGIN: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('should not display institution login option when no secondary providers are present', () => {
|
||||
const root = mount(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(root.text().includes('Use my university info')).toBe(false);
|
||||
});
|
||||
|
||||
it('should not show sign-in header and enterprise login once user authenticated through SSO', () => {
|
||||
mergeConfig({
|
||||
DISABLE_ENTERPRISE_LOGIN: '',
|
||||
});
|
||||
|
||||
it('should display sign-in header only when primary or secondary providers are available.', () => {
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
commonComponents: {
|
||||
...initialState.commonComponents,
|
||||
thirdPartyAuthContext: {
|
||||
...initialState.commonComponents.thirdPartyAuthContext,
|
||||
providers: [{
|
||||
...ssoProvider,
|
||||
}],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { queryByText } = render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(queryByText('Company or school credentials')).toBeNull();
|
||||
expect(queryByText('Or sign in with:')).toBeNull();
|
||||
expect(queryByText('Institution/campus credentials')).toBeNull();
|
||||
});
|
||||
|
||||
it('should hide sign-in header and enterprise login upon successful SSO authentication', () => {
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
commonComponents: {
|
||||
...initialState.commonComponents,
|
||||
thirdPartyAuthContext: {
|
||||
...initialState.commonComponents.thirdPartyAuthContext,
|
||||
providers: [ssoProvider],
|
||||
secondaryProviders: [secondaryProviders],
|
||||
currentProvider: 'Apple',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const loginPage = mount(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(loginPage.text().includes('Company or school credentials')).toBe(false);
|
||||
expect(loginPage.text().includes('Or sign in with:')).toBe(false);
|
||||
const { queryByText } = render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(queryByText('Company or school credentials')).toBeNull();
|
||||
expect(queryByText('Or sign in with:')).toBeNull();
|
||||
});
|
||||
|
||||
it('should show sign-in header providers (ENABLE ENTERPRISE LOGIN)', () => {
|
||||
mergeConfig({
|
||||
DISABLE_ENTERPRISE_LOGIN: '',
|
||||
});
|
||||
// ******** test enterprise login enabled scenarios ********
|
||||
|
||||
it('should show sign-in header for enterprise login', () => {
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
commonComponents: {
|
||||
...initialState.commonComponents,
|
||||
thirdPartyAuthContext: {
|
||||
...initialState.commonComponents.thirdPartyAuthContext,
|
||||
providers: [{
|
||||
...ssoProvider,
|
||||
}],
|
||||
providers: [ssoProvider],
|
||||
secondaryProviders: [secondaryProviders],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const loginPage = mount(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(loginPage.text().includes('Or sign in with:')).toBe(true);
|
||||
expect(loginPage.text().includes('Company or school credentials')).toBe(true);
|
||||
expect(loginPage.text().includes('Institution/campus credentials')).toBe(false);
|
||||
const { queryByText } = render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(queryByText('Or sign in with:')).toBeDefined();
|
||||
expect(queryByText('Company or school credentials')).toBeDefined();
|
||||
expect(queryByText('Institution/campus credentials')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should show sign-in header with providers (DISABLE ENTERPRISE LOGIN)', () => {
|
||||
// ******** test enterprise login disabled scenarios ********
|
||||
|
||||
it('should show sign-in header for institution login if enterprise login is disabled', () => {
|
||||
mergeConfig({
|
||||
DISABLE_ENTERPRISE_LOGIN: true,
|
||||
});
|
||||
@@ -270,63 +346,16 @@ describe('LoginPage', () => {
|
||||
...initialState.commonComponents,
|
||||
thirdPartyAuthContext: {
|
||||
...initialState.commonComponents.thirdPartyAuthContext,
|
||||
providers: [{
|
||||
...ssoProvider,
|
||||
}],
|
||||
providers: [ssoProvider],
|
||||
secondaryProviders: [secondaryProviders],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const loginPage = mount(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(loginPage.text().includes('Or sign in with:')).toBe(true);
|
||||
expect(loginPage.text().includes('Company or school credentials')).toBe(false);
|
||||
expect(loginPage.text().includes('Institution/campus credentials')).toBe(false);
|
||||
|
||||
mergeConfig({
|
||||
DISABLE_ENTERPRISE_LOGIN: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('should not show sign-in header without Providers and secondary Providers (ENABLE ENTERPRISE LOGIN)', () => {
|
||||
mergeConfig({
|
||||
DISABLE_ENTERPRISE_LOGIN: '',
|
||||
});
|
||||
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
commonComponents: {
|
||||
...initialState.commonComponents,
|
||||
thirdPartyAuthContext: {
|
||||
...initialState.commonComponents.thirdPartyAuthContext,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const loginPage = mount(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(loginPage.text().includes('Or sign in with:')).toBe(false);
|
||||
expect(loginPage.text().includes('Company or school credentials')).toBe(false);
|
||||
expect(loginPage.text().includes('Institution/campus credentials')).toBe(false);
|
||||
});
|
||||
|
||||
it('should not show sign-in header without Providers and secondary Providers (DISABLE ENTERPRISE LOGIN)', () => {
|
||||
mergeConfig({
|
||||
DISABLE_ENTERPRISE_LOGIN: true,
|
||||
});
|
||||
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
commonComponents: {
|
||||
...initialState.commonComponents,
|
||||
thirdPartyAuthContext: {
|
||||
...initialState.commonComponents.thirdPartyAuthContext,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const loginPage = mount(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(loginPage.text().includes('Or sign in with:')).toBe(false);
|
||||
expect(loginPage.text().includes('Company or school credentials')).toBe(false);
|
||||
expect(loginPage.text().includes('Institution/campus credentials')).toBe(false);
|
||||
const { queryByText } = render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(queryByText('Or sign in with:')).toBeDefined();
|
||||
expect(queryByText('Company or school credentials')).toBeNull();
|
||||
expect(queryByText('Institution/campus credentials')).toBeDefined();
|
||||
|
||||
mergeConfig({
|
||||
DISABLE_ENTERPRISE_LOGIN: '',
|
||||
@@ -351,40 +380,48 @@ describe('LoginPage', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const loginPage = mount(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(loginPage.text().includes('Or sign in with:')).toBe(true);
|
||||
expect(loginPage.text().includes('Institution/campus credentials')).toBe(true);
|
||||
const { queryByText } = render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(queryByText('Or sign in with:')).toBeDefined();
|
||||
expect(queryByText('Institution/campus credentials')).toBeDefined();
|
||||
|
||||
mergeConfig({
|
||||
DISABLE_ENTERPRISE_LOGIN: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('should show sign-in header with Providers and secondary Providers', () => {
|
||||
mergeConfig({
|
||||
DISABLE_ENTERPRISE_LOGIN: true,
|
||||
});
|
||||
|
||||
it('should not show sign-in header without primary or secondary providers', () => {
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
commonComponents: {
|
||||
...initialState.commonComponents,
|
||||
thirdPartyAuthContext: {
|
||||
...initialState.commonComponents.thirdPartyAuthContext,
|
||||
providers: [{
|
||||
...ssoProvider,
|
||||
}],
|
||||
secondaryProviders: [{
|
||||
...secondaryProviders,
|
||||
}],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const loginPage = mount(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(loginPage.text().includes('Or sign in with:')).toBe(true);
|
||||
expect(loginPage.text().includes('Company or school credentials')).toBe(false);
|
||||
expect(loginPage.text().includes('Institution/campus credentials')).toBe(true);
|
||||
const { queryByText } = render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(queryByText('Or sign in with:')).toBeNull();
|
||||
expect(queryByText('Institution/campus credentials')).toBeNull();
|
||||
expect(queryByText('Company or school credentials')).toBeNull();
|
||||
});
|
||||
|
||||
it('should show enterprise login if even if only secondary providers are available', () => {
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
commonComponents: {
|
||||
...initialState.commonComponents,
|
||||
thirdPartyAuthContext: {
|
||||
...initialState.commonComponents.thirdPartyAuthContext,
|
||||
secondaryProviders: [secondaryProviders],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { queryByText } = render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(queryByText('Or sign in with:')).toBeDefined();
|
||||
expect(queryByText('Company or school credentials')).toBeNull();
|
||||
expect(queryByText('Institution/campus credentials')).toBeDefined();
|
||||
|
||||
mergeConfig({
|
||||
DISABLE_ENTERPRISE_LOGIN: '',
|
||||
@@ -393,30 +430,22 @@ describe('LoginPage', () => {
|
||||
|
||||
// ******** test alert messages ********
|
||||
|
||||
it('should match login error message', () => {
|
||||
const errorMessage = 'An error has occurred. Try refreshing the page, or check your internet connection.';
|
||||
it('should match login internal server error message', () => {
|
||||
const expectedMessage = 'We couldn\'t sign you in.'
|
||||
+ 'An error has occurred. Try refreshing the page, or check your internet connection.';
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
login: {
|
||||
...initialState.login,
|
||||
loginError: { errorCode: INTERNAL_SERVER_ERROR },
|
||||
loginErrorCode: INTERNAL_SERVER_ERROR,
|
||||
},
|
||||
});
|
||||
|
||||
const loginPage = mount(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(loginPage.find('#login-failure-alert').first().text()).toEqual(`We couldn't sign you in.${errorMessage}`);
|
||||
});
|
||||
|
||||
it('should match account activation message', () => {
|
||||
const activationMessage = 'Success! You have activated your account.'
|
||||
+ 'You will now receive email updates and alerts from us related '
|
||||
+ 'to the courses you are enrolled in. Sign in to continue.';
|
||||
|
||||
delete window.location;
|
||||
window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: '?account_activation_status=success' };
|
||||
|
||||
const loginPage = mount(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(loginPage.find('div#account-activation-message').text()).toEqual(activationMessage);
|
||||
render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(screen.getByText(
|
||||
'',
|
||||
{ selector: '#login-failure-alert' },
|
||||
).textContent).toEqual(`${expectedMessage}`);
|
||||
});
|
||||
|
||||
it('should match third party auth alert', () => {
|
||||
@@ -436,11 +465,14 @@ describe('LoginPage', () => {
|
||||
+ 'linked '}${ getConfig().SITE_NAME } account. To link your accounts, sign in now using your ${
|
||||
getConfig().SITE_NAME } password.`;
|
||||
|
||||
const loginPage = mount(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(loginPage.find('#tpa-alert').find('p').text()).toEqual(expectedMessage);
|
||||
render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(screen.getByText(
|
||||
'',
|
||||
{ selector: '#tpa-alert' },
|
||||
).textContent).toEqual(expectedMessage);
|
||||
});
|
||||
|
||||
it('should show tpa authentication fails error message', () => {
|
||||
it('should show third party authentication failure message', () => {
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
commonComponents: {
|
||||
@@ -452,9 +484,11 @@ describe('LoginPage', () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const loginPage = mount(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(loginPage.find('#login-failure-alert').find('p').text()).toContain('An error occurred');
|
||||
render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(screen.getByText(
|
||||
'',
|
||||
{ selector: '#login-failure-alert' },
|
||||
).textContent).toContain('An error occurred');
|
||||
});
|
||||
|
||||
it('should match invalid login form error message', () => {
|
||||
@@ -463,33 +497,36 @@ describe('LoginPage', () => {
|
||||
...initialState,
|
||||
login: {
|
||||
...initialState.login,
|
||||
loginError: { errorCode: 'invalid-form' },
|
||||
loginErrorCode: 'invalid-form',
|
||||
},
|
||||
});
|
||||
|
||||
const loginPage = mount(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(loginPage.find('#login-failure-alert p').first().text()).toEqual(errorMessage);
|
||||
render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(screen.getByText(
|
||||
'',
|
||||
{ selector: '#login-failure-alert' },
|
||||
).textContent).toContain(errorMessage);
|
||||
});
|
||||
|
||||
// ******** test redirection ********
|
||||
|
||||
it('should redirect to url returned by login endpoint', () => {
|
||||
const dashboardUrl = 'http://localhost:18000/enterprise/select/active/?success_url=/dashboard';
|
||||
it('should redirect to url returned by login endpoint after successful authentication', () => {
|
||||
const dashboardURL = 'https://test.com/testing-dashboard/';
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
login: {
|
||||
...initialState.login,
|
||||
loginResult: {
|
||||
success: true,
|
||||
redirectUrl: dashboardUrl,
|
||||
redirectUrl: dashboardURL,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
delete window.location;
|
||||
window.location = { href: getConfig().BASE_URL };
|
||||
renderer.create(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(window.location.href).toBe(dashboardUrl);
|
||||
render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(window.location.href).toBe(dashboardURL);
|
||||
});
|
||||
|
||||
it('should redirect to finishAuthUrl upon successful login via SSO', () => {
|
||||
@@ -515,26 +552,18 @@ describe('LoginPage', () => {
|
||||
delete window.location;
|
||||
window.location = { href: getConfig().BASE_URL };
|
||||
|
||||
renderer.create(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + authCompleteUrl);
|
||||
});
|
||||
|
||||
it('should redirect to social auth provider url on SSO button click', () => {
|
||||
mergeConfig({
|
||||
DISABLE_ENTERPRISE_LOGIN: 'true',
|
||||
});
|
||||
|
||||
const loginUrl = '/auth/login/apple-id/?auth_entry=login&next=/dashboard';
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
commonComponents: {
|
||||
...initialState.commonComponents,
|
||||
thirdPartyAuthContext: {
|
||||
...initialState.commonComponents.thirdPartyAuthContext,
|
||||
providers: [{
|
||||
...ssoProvider,
|
||||
loginUrl,
|
||||
}],
|
||||
providers: [ssoProvider],
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -542,14 +571,37 @@ describe('LoginPage', () => {
|
||||
delete window.location;
|
||||
window.location = { href: getConfig().BASE_URL };
|
||||
|
||||
const loginPage = mount(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
|
||||
loginPage.find('button#oa2-apple-id').simulate('click');
|
||||
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + loginUrl);
|
||||
fireEvent.click(screen.getByText(
|
||||
'',
|
||||
{ selector: '#oa2-apple-id' },
|
||||
));
|
||||
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + ssoProvider.loginUrl);
|
||||
});
|
||||
|
||||
mergeConfig({
|
||||
DISABLE_ENTERPRISE_LOGIN: '',
|
||||
it('should redirect to finishAuthUrl upon successful authentication via SSO', () => {
|
||||
const finishAuthUrl = '/auth/complete/google-oauth2/';
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
login: {
|
||||
...initialState.login,
|
||||
loginResult: { success: true, redirectUrl: '' },
|
||||
},
|
||||
commonComponents: {
|
||||
...initialState.commonComponents,
|
||||
thirdPartyAuthContext: {
|
||||
...initialState.commonComponents.thirdPartyAuthContext,
|
||||
finishAuthUrl,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
delete window.location;
|
||||
window.location = { href: getConfig().BASE_URL };
|
||||
|
||||
render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + finishAuthUrl);
|
||||
});
|
||||
|
||||
// ******** test hinted third party auth ********
|
||||
@@ -570,9 +622,35 @@ describe('LoginPage', () => {
|
||||
delete window.location;
|
||||
window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: `?next=/dashboard&tpa_hint=${ssoProvider.id}` };
|
||||
|
||||
const loginPage = mount(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(loginPage.find(`button#${ssoProvider.id}`).find('span').text()).toEqual(ssoProvider.name);
|
||||
expect(loginPage.find(`button#${ssoProvider.id}`).hasClass(`btn-tpa btn-${ssoProvider.id}`)).toEqual(true);
|
||||
render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(screen.getByText(
|
||||
'',
|
||||
{ selector: `#${ssoProvider.id}` },
|
||||
).textContent).toEqual(ssoProvider.name);
|
||||
expect(screen.getByText(
|
||||
'',
|
||||
{ selector: `.btn-${ssoProvider.id}` },
|
||||
)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render the skeleton when third party status is pending', () => {
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
commonComponents: {
|
||||
...initialState.commonComponents,
|
||||
thirdPartyAuthContext: {
|
||||
...initialState.commonComponents.thirdPartyAuthContext,
|
||||
providers: [ssoProvider],
|
||||
},
|
||||
thirdPartyAuthApiStatus: PENDING_STATE,
|
||||
},
|
||||
});
|
||||
|
||||
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} />));
|
||||
expect(container.querySelector('.react-loading-skeleton')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render tpa button for tpa_hint id matching one of the secondary providers', () => {
|
||||
@@ -593,15 +671,11 @@ describe('LoginPage', () => {
|
||||
window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: `?next=/dashboard&tpa_hint=${secondaryProviders.id}` };
|
||||
secondaryProviders.iconImage = null;
|
||||
|
||||
mount(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(window.location.href).toEqual(getConfig().LMS_BASE_URL + secondaryProviders.loginUrl);
|
||||
});
|
||||
|
||||
it('should render regular tpa button for invalid tpa_hint value', () => {
|
||||
mergeConfig({
|
||||
DISABLE_ENTERPRISE_LOGIN: 'true',
|
||||
});
|
||||
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
commonComponents: {
|
||||
@@ -617,15 +691,15 @@ describe('LoginPage', () => {
|
||||
delete window.location;
|
||||
window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: '?next=/dashboard&tpa_hint=invalid' };
|
||||
|
||||
const loginPage = mount(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(loginPage.find(`button#${ssoProvider.id}`).find('span#provider-name').text()).toEqual(`${ssoProvider.name}`);
|
||||
const { container } = render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(container.querySelector(`#${ssoProvider.id}`).querySelector('#provider-name').textContent).toEqual(`${ssoProvider.name}`);
|
||||
|
||||
mergeConfig({
|
||||
DISABLE_ENTERPRISE_LOGIN: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('should render other ways to sign in button', () => {
|
||||
it('should render "other ways to sign in" button on the tpa_hint page', () => {
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
commonComponents: {
|
||||
@@ -641,14 +715,17 @@ describe('LoginPage', () => {
|
||||
delete window.location;
|
||||
window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: `?tpa_hint=${ssoProvider.id}` };
|
||||
|
||||
const loginPage = mount(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(loginPage.find('button#other-ways-to-sign-in').text()).toEqual('Show me other ways to sign in or register');
|
||||
render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(screen.getByText(
|
||||
'Show me other ways to sign in or register',
|
||||
).textContent).toBeDefined();
|
||||
});
|
||||
|
||||
it('should render other ways to sign in button when public account creation disabled', () => {
|
||||
it('should render other ways to sign in button when public account creation is disabled', () => {
|
||||
mergeConfig({
|
||||
ALLOW_PUBLIC_ACCOUNT_CREATION: false,
|
||||
});
|
||||
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
commonComponents: {
|
||||
@@ -664,106 +741,93 @@ describe('LoginPage', () => {
|
||||
delete window.location;
|
||||
window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: `?tpa_hint=${ssoProvider.id}` };
|
||||
|
||||
const loginPage = mount(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(loginPage.find('button#other-ways-to-sign-in').text()).toEqual('Show me other ways to sign in');
|
||||
render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(screen.getByText(
|
||||
'Show me other ways to sign in',
|
||||
).textContent).toBeDefined();
|
||||
});
|
||||
|
||||
// ******** miscellaneous tests ********
|
||||
|
||||
it('should send page event when login page is rendered', () => {
|
||||
mount(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'login');
|
||||
});
|
||||
|
||||
it('tests that form is only scrollable on form submission', () => {
|
||||
const loginPage = mount(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
|
||||
loginPage.find('input#password').simulate('change', { target: { value: 'test', name: 'password' } });
|
||||
loginPage.find('button.btn-brand').simulate('click');
|
||||
|
||||
expect(loginPage.find(<IntlLoginFailureMessage />)).toBeTruthy();
|
||||
expect(loginPage.find('LoginPage').state('isSubmitted')).toEqual(true);
|
||||
});
|
||||
|
||||
it('should reset login form errors', () => {
|
||||
const errorState = { emailOrUsername: '', password: '' };
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
|
||||
const loginPage = (mount(reduxWrapper(<IntlLoginPage {...props} />))).find('LoginPage');
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith(loginRequestReset());
|
||||
expect(loginPage.state('errors')).toEqual(errorState);
|
||||
});
|
||||
|
||||
// persists form data tests
|
||||
|
||||
it('should set errors in redux store on submit form for invalid input', () => {
|
||||
const formData = {
|
||||
errors: {
|
||||
emailOrUsername: 'Enter your username or email',
|
||||
password: 'Enter your password',
|
||||
},
|
||||
};
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
const loginPage = mount(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
|
||||
loginPage.find('input#emailOrUsername').simulate('change', { target: { value: '' } });
|
||||
loginPage.find('input#password').simulate('change', { target: { value: '' } });
|
||||
loginPage.find('button.btn-brand').simulate('click');
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith(setLoginFormData(formData));
|
||||
});
|
||||
|
||||
it('should set form data in redux store on onBlur', () => {
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
|
||||
const loginPage = mount(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
loginPage.find('input#emailOrUsername').simulate('blur');
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith(setLoginFormData({ emailOrUsername: '' }));
|
||||
});
|
||||
|
||||
it('should clear form field errors in redux store on onFocus', () => {
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
|
||||
const loginPage = mount(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
loginPage.find('input#emailOrUsername').simulate('focus');
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith(setLoginFormData({
|
||||
errors: {
|
||||
...loginFormData.errors,
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
||||
it('should update form fields state if updated in redux store', () => {
|
||||
const nextProps = {
|
||||
loginFormData: {
|
||||
emailOrUsername: 'john_doe',
|
||||
password: 'password1',
|
||||
},
|
||||
};
|
||||
|
||||
const loginPage = mount(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
loginPage.find('LoginPage').instance().shouldComponentUpdate(nextProps);
|
||||
|
||||
expect(loginPage.find('LoginPage').state('emailOrUsername')).toEqual('john_doe');
|
||||
expect(loginPage.find('LoginPage').state('password')).toEqual('password1');
|
||||
});
|
||||
|
||||
it('should update reset password value when unmount called', () => {
|
||||
it('tests that form is in invalid state when it is submitted', () => {
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
login: {
|
||||
...initialState.login,
|
||||
resetPassword: true,
|
||||
shouldBackupState: true,
|
||||
},
|
||||
});
|
||||
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
const loginPage = mount(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
loginPage.unmount();
|
||||
render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(store.dispatch).toHaveBeenCalledWith(backupLoginFormBegin(
|
||||
{
|
||||
formFields: {
|
||||
emailOrUsername: '', password: '',
|
||||
},
|
||||
errors: {
|
||||
emailOrUsername: '', password: '',
|
||||
},
|
||||
},
|
||||
));
|
||||
});
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith(loginRemovePasswordResetBanner());
|
||||
it('should send track event when forgot password link is clicked', () => {
|
||||
render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
fireEvent.click(screen.getByText(
|
||||
'Forgot password',
|
||||
{ selector: '#forgot-password' },
|
||||
));
|
||||
|
||||
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.password-reset_form.toggled', { category: 'user-engagement' });
|
||||
});
|
||||
|
||||
it('should backup the login form state when shouldBackupState is true', () => {
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
login: {
|
||||
...initialState.login,
|
||||
shouldBackupState: true,
|
||||
},
|
||||
});
|
||||
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(store.dispatch).toHaveBeenCalledWith(backupLoginFormBegin(
|
||||
{
|
||||
formFields: {
|
||||
emailOrUsername: '', password: '',
|
||||
},
|
||||
errors: {
|
||||
emailOrUsername: '', password: '',
|
||||
},
|
||||
},
|
||||
));
|
||||
});
|
||||
|
||||
it('should update form fields state if updated in redux store', () => {
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
login: {
|
||||
...initialState.login,
|
||||
loginFormData: {
|
||||
formFields: {
|
||||
emailOrUsername: 'john_doe', password: 'test-password',
|
||||
},
|
||||
errors: {
|
||||
emailOrUsername: '', password: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { container } = render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(container.querySelector('input#emailOrUsername').value).toEqual('john_doe');
|
||||
expect(container.querySelector('input#password').value).toEqual('test-password');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,8 +9,8 @@ import {
|
||||
Icon,
|
||||
Tab,
|
||||
Tabs,
|
||||
} from '@edx/paragon';
|
||||
import { ChevronLeft } from '@edx/paragon/icons';
|
||||
} from '@openedx/paragon';
|
||||
import { ChevronLeft } from '@openedx/paragon/icons';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Navigate, useNavigate } from 'react-router-dom';
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
getTpaHint, getTpaProvider, updatePathWithQueryParams,
|
||||
} from '../data/utils';
|
||||
import { LoginPage } from '../login';
|
||||
import { backupLoginForm } from '../login/data/actions';
|
||||
import { RegistrationPage } from '../register';
|
||||
import { backupRegistrationForm } from '../register/data/actions';
|
||||
|
||||
@@ -70,6 +71,8 @@ const Logistration = (props) => {
|
||||
props.clearThirdPartyAuthContextErrorMessage();
|
||||
if (tabKey === LOGIN_PAGE) {
|
||||
props.backupRegistrationForm();
|
||||
} else if (tabKey === REGISTER_PAGE) {
|
||||
props.backupLoginForm();
|
||||
}
|
||||
setKey(tabKey);
|
||||
};
|
||||
@@ -150,6 +153,7 @@ const Logistration = (props) => {
|
||||
|
||||
Logistration.propTypes = {
|
||||
selectedPage: PropTypes.string,
|
||||
backupLoginForm: PropTypes.func.isRequired,
|
||||
backupRegistrationForm: PropTypes.func.isRequired,
|
||||
clearThirdPartyAuthContextErrorMessage: PropTypes.func.isRequired,
|
||||
tpaProviders: PropTypes.shape({
|
||||
@@ -176,6 +180,7 @@ const mapStateToProps = state => ({
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
{
|
||||
backupLoginForm,
|
||||
backupRegistrationForm,
|
||||
clearThirdPartyAuthContextErrorMessage,
|
||||
},
|
||||
|
||||
@@ -4,16 +4,16 @@ 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 { mount } from 'enzyme';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import configureStore from 'redux-mock-store';
|
||||
|
||||
import Logistration from './Logistration';
|
||||
import { clearThirdPartyAuthContextErrorMessage } from '../common-components/data/actions';
|
||||
import { RenderInstitutionButton } from '../common-components/InstitutionLogistration';
|
||||
import {
|
||||
COMPLETE_STATE, LOGIN_PAGE, REGISTER_PAGE,
|
||||
} from '../data/constants';
|
||||
import { backupLoginForm } from '../login/data/actions';
|
||||
import { backupRegistrationForm } from '../register/data/actions';
|
||||
|
||||
jest.mock('@edx/frontend-platform/analytics', () => ({
|
||||
@@ -99,16 +99,16 @@ describe('Logistration', () => {
|
||||
ALLOW_PUBLIC_ACCOUNT_CREATION: true,
|
||||
});
|
||||
|
||||
const logistration = mount(reduxWrapper(<IntlLogistration />));
|
||||
const { container } = render(reduxWrapper(<IntlLogistration />));
|
||||
|
||||
expect(logistration.find('#main-content').find('RegistrationPage').exists()).toBeTruthy();
|
||||
expect(container.querySelector('RegistrationPage')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should render login page', () => {
|
||||
const props = { selectedPage: LOGIN_PAGE };
|
||||
const logistration = mount(reduxWrapper(<IntlLogistration {...props} />));
|
||||
const { container } = render(reduxWrapper(<IntlLogistration {...props} />));
|
||||
|
||||
expect(logistration.find('#main-content').find('LoginPage').exists()).toBeTruthy();
|
||||
expect(container.querySelector('LoginPage')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should render login/register headings when show registration links is disabled', () => {
|
||||
@@ -117,18 +117,18 @@ describe('Logistration', () => {
|
||||
});
|
||||
|
||||
let props = { selectedPage: LOGIN_PAGE };
|
||||
let logistration = mount(reduxWrapper(<IntlLogistration {...props} />));
|
||||
const { rerender } = render(reduxWrapper(<IntlLogistration {...props} />));
|
||||
|
||||
// verifying sign in heading
|
||||
expect(logistration.find('#main-content').find('h3').text()).toEqual('Sign in');
|
||||
expect(screen.getByRole('heading', { level: 3 }).textContent).toEqual('Sign in');
|
||||
|
||||
// register page is still accessible when SHOW_REGISTRATION_LINKS is false
|
||||
// but it needs to be accessed directly
|
||||
props = { selectedPage: REGISTER_PAGE };
|
||||
logistration = mount(reduxWrapper(<IntlLogistration {...props} />));
|
||||
rerender(reduxWrapper(<IntlLogistration {...props} />));
|
||||
|
||||
// verifying register heading
|
||||
expect(logistration.find('#main-content').find('h3').text()).toEqual('Register');
|
||||
expect(screen.getByRole('heading', { level: 3 }).textContent).toEqual('Register');
|
||||
});
|
||||
|
||||
it('should render only login page when public account creation is disabled', () => {
|
||||
@@ -152,14 +152,14 @@ describe('Logistration', () => {
|
||||
});
|
||||
|
||||
const props = { selectedPage: LOGIN_PAGE };
|
||||
const logistration = mount(reduxWrapper(<IntlLogistration {...props} />));
|
||||
const { container } = render(reduxWrapper(<IntlLogistration {...props} />));
|
||||
|
||||
// verifying sign in heading for institution login false
|
||||
expect(logistration.find('#main-content').find('h3').text()).toEqual('Sign in');
|
||||
expect(screen.getByRole('heading', { level: 3 }).textContent).toEqual('Sign in');
|
||||
|
||||
// verifying tabs heading for institution login true
|
||||
logistration.find(RenderInstitutionButton).simulate('click', { institutionLogin: true });
|
||||
expect(logistration.find('#controlled-tab').exists()).toBeTruthy();
|
||||
fireEvent.click(screen.getByRole('link'));
|
||||
expect(container.querySelector('#controlled-tab')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should display institution login option when secondary providers are present', () => {
|
||||
@@ -182,12 +182,12 @@ describe('Logistration', () => {
|
||||
});
|
||||
|
||||
const props = { selectedPage: LOGIN_PAGE };
|
||||
const logistration = mount(reduxWrapper(<IntlLogistration {...props} />));
|
||||
expect(logistration.text().includes('Institution/campus credentials')).toBe(true);
|
||||
render(reduxWrapper(<IntlLogistration {...props} />));
|
||||
expect(screen.getByText('Institution/campus credentials')).toBeDefined();
|
||||
|
||||
// on clicking "Institution/campus credentials" button, it should display institution login page
|
||||
logistration.find(RenderInstitutionButton).simulate('click', { institutionLogin: true });
|
||||
expect(logistration.text().includes('Test University')).toBe(true);
|
||||
fireEvent.click(screen.getByText('Institution/campus credentials'));
|
||||
expect(screen.getByText('Test University')).toBeDefined();
|
||||
|
||||
mergeConfig({
|
||||
DISABLE_ENTERPRISE_LOGIN: '',
|
||||
@@ -213,8 +213,8 @@ describe('Logistration', () => {
|
||||
});
|
||||
|
||||
const props = { selectedPage: LOGIN_PAGE };
|
||||
const logistration = mount(reduxWrapper(<IntlLogistration {...props} />));
|
||||
logistration.find(RenderInstitutionButton).simulate('click', { institutionLogin: true });
|
||||
render(reduxWrapper(<IntlLogistration {...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');
|
||||
@@ -245,9 +245,9 @@ describe('Logistration', () => {
|
||||
delete window.location;
|
||||
window.location = { hostname: getConfig().SITE_NAME, href: getConfig().BASE_URL };
|
||||
|
||||
const root = mount(reduxWrapper(<IntlLogistration />));
|
||||
root.find(RenderInstitutionButton).simulate('click', { institutionLogin: true });
|
||||
expect(root.text().includes('Test University')).toBe(true);
|
||||
render(reduxWrapper(<IntlLogistration />));
|
||||
fireEvent.click(screen.getByText('Institution/campus credentials'));
|
||||
expect(screen.getByText('Test University')).toBeDefined();
|
||||
|
||||
mergeConfig({
|
||||
DISABLE_ENTERPRISE_LOGIN: '',
|
||||
@@ -256,15 +256,21 @@ describe('Logistration', () => {
|
||||
|
||||
it('should fire action to backup registration form on tab click', () => {
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
const logistration = mount(reduxWrapper(<IntlLogistration />));
|
||||
logistration.find('a[data-rb-event-key="/login"]').simulate('click');
|
||||
const { container } = render(reduxWrapper(<IntlLogistration />));
|
||||
fireEvent.click(container.querySelector('a[data-rb-event-key="/login"]'));
|
||||
expect(store.dispatch).toHaveBeenCalledWith(backupRegistrationForm());
|
||||
});
|
||||
it('should fire action to backup login form on tab click', () => {
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
const { container } = render(reduxWrapper(<IntlLogistration />));
|
||||
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 logistration = mount(reduxWrapper(<IntlLogistration />));
|
||||
logistration.find('a[data-rb-event-key="/login"]').simulate('click');
|
||||
const { container } = render(reduxWrapper(<IntlLogistration />));
|
||||
fireEvent.click(container.querySelector('a[data-rb-event-key="/login"]'));
|
||||
expect(store.dispatch).toHaveBeenCalledWith(clearThirdPartyAuthContextErrorMessage());
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,8 +16,8 @@ import {
|
||||
Hyperlink,
|
||||
Spinner,
|
||||
StatefulButton,
|
||||
} from '@edx/paragon';
|
||||
import { Error } from '@edx/paragon/icons';
|
||||
} from '@openedx/paragon';
|
||||
import { Error } from '@openedx/paragon/icons';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
@@ -156,6 +156,7 @@ const ProgressiveProfiling = (props) => {
|
||||
isGenderSelected: !!values.gender,
|
||||
isYearOfBirthSelected: !!values.year_of_birth,
|
||||
isLevelOfEducationSelected: !!values.level_of_education,
|
||||
isWorkExperienceSelected: !!values.work_experience,
|
||||
host: queryParams?.host || '',
|
||||
},
|
||||
);
|
||||
@@ -195,7 +196,7 @@ const ProgressiveProfiling = (props) => {
|
||||
});
|
||||
|
||||
return (
|
||||
<BaseContainer showWelcomeBanner username={authenticatedUser?.username}>
|
||||
<BaseContainer showWelcomeBanner fullName={authenticatedUser?.fullName || authenticatedUser?.username}>
|
||||
<Helmet>
|
||||
<title>{formatMessage(messages['progressive.profiling.page.title'],
|
||||
{ siteName: getConfig().SITE_NAME })}
|
||||
@@ -284,6 +285,7 @@ ProgressiveProfiling.propTypes = {
|
||||
authenticatedUser: PropTypes.shape({
|
||||
username: PropTypes.string,
|
||||
userId: PropTypes.number,
|
||||
fullName: PropTypes.string,
|
||||
}),
|
||||
showError: PropTypes.bool,
|
||||
shouldRedirect: PropTypes.bool,
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { ActionRow, Button, ModalDialog } from '@edx/paragon';
|
||||
import { ActionRow, Button, ModalDialog } from '@openedx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
@@ -173,6 +173,7 @@ describe('ProgressiveProfilingTests', () => {
|
||||
isGenderSelected: false,
|
||||
isYearOfBirthSelected: false,
|
||||
isLevelOfEducationSelected: false,
|
||||
isWorkExperienceSelected: false,
|
||||
host: '',
|
||||
};
|
||||
delete window.location;
|
||||
@@ -346,6 +347,7 @@ describe('ProgressiveProfilingTests', () => {
|
||||
isGenderSelected: false,
|
||||
isYearOfBirthSelected: false,
|
||||
isLevelOfEducationSelected: false,
|
||||
isWorkExperienceSelected: false,
|
||||
host: 'http://example.com',
|
||||
};
|
||||
delete window.location;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Badge, Card, Hyperlink } from '@edx/paragon';
|
||||
import { Badge, Card, Hyperlink } from '@openedx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { truncateText } from '../../data/utils';
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
Image, Skeleton,
|
||||
StatefulButton,
|
||||
useMediaQuery,
|
||||
} from '@edx/paragon';
|
||||
} from '@openedx/paragon';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Skeleton } from '@edx/paragon';
|
||||
import { Skeleton } from '@openedx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import loadingCoursesPlaceholders from '../data/loadingCoursesPlaceholders';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Skeleton } from '@edx/paragon';
|
||||
import { Skeleton } from '@openedx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import loadingCoursesPlaceholders from '../data/loadingCoursesPlaceholders';
|
||||
|
||||
@@ -13,8 +13,8 @@ jest.mock('react-router-dom', () => ({
|
||||
useLocation: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@edx/paragon', () => ({
|
||||
...jest.requireActual('@edx/paragon'),
|
||||
jest.mock('@openedx/paragon', () => ({
|
||||
...jest.requireActual('@openedx/paragon'),
|
||||
useMediaQuery: jest.fn(),
|
||||
}));
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ 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 { useMediaQuery } from '@edx/paragon';
|
||||
import { useMediaQuery } from '@openedx/paragon';
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import configureStore from 'redux-mock-store';
|
||||
@@ -28,8 +28,8 @@ jest.mock('react-router-dom', () => ({
|
||||
useLocation: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@edx/paragon', () => ({
|
||||
...jest.requireActual('@edx/paragon'),
|
||||
jest.mock('@openedx/paragon', () => ({
|
||||
...jest.requireActual('@openedx/paragon'),
|
||||
useMediaQuery: jest.fn(),
|
||||
}));
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { FormAutosuggest, FormAutosuggestOption, FormControlFeedback } from '@edx/paragon';
|
||||
import { FormAutosuggest, FormAutosuggestOption, FormControlFeedback } from '@openedx/paragon';
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
@@ -31,6 +31,13 @@ const CountryField = (props) => {
|
||||
} = props;
|
||||
const { formatMessage } = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const countryFieldValue = {
|
||||
userProvidedText: selectedCountry.displayValue,
|
||||
selectionValue: selectedCountry.displayValue,
|
||||
selectionId: selectedCountry.countryCode,
|
||||
};
|
||||
|
||||
const backendCountryCode = useSelector(state => state.register.backendCountryCode);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -49,6 +56,11 @@ const CountryField = (props) => {
|
||||
{ target: { name: 'country' } },
|
||||
{ countryCode, displayValue: countryDisplayValue },
|
||||
);
|
||||
} else if (!selectedCountry.displayValue) {
|
||||
onChangeHandler(
|
||||
{ target: { name: 'country' } },
|
||||
{ countryCode: '', displayValue: '' },
|
||||
);
|
||||
}
|
||||
}, [backendCountryCode, countryList]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
@@ -60,18 +72,12 @@ const CountryField = (props) => {
|
||||
|
||||
const { value } = event.target;
|
||||
|
||||
const { countryCode, displayValue, error } = validateCountryField(
|
||||
const { error } = validateCountryField(
|
||||
value.trim(), countryList, formatMessage(messages['empty.country.field.error']), formatMessage(messages['invalid.country.field.error']),
|
||||
);
|
||||
|
||||
onChangeHandler({ target: { name: 'country' } }, { countryCode, displayValue });
|
||||
handleErrorChange('country', error);
|
||||
};
|
||||
|
||||
const handleSelected = (value) => {
|
||||
handleOnBlur({ target: { name: 'country', value } });
|
||||
};
|
||||
|
||||
const handleOnFocus = (event) => {
|
||||
handleErrorChange('country', '');
|
||||
dispatch(clearRegistrationBackendError('country'));
|
||||
@@ -79,11 +85,19 @@ const CountryField = (props) => {
|
||||
};
|
||||
|
||||
const handleOnChange = (value) => {
|
||||
onChangeHandler({ target: { name: 'country' } }, { countryCode: '', displayValue: value });
|
||||
onChangeHandler({ target: { name: 'country' } }, { countryCode: value.selectionId, displayValue: value.userProvidedText });
|
||||
|
||||
// We have put this check because proviously we also had onSelected event handler and we call
|
||||
// the onBlur on that event handler but now there is no such handler and we only have
|
||||
// onChange so we check the is there is proper sectionId which only be
|
||||
// proper one when we select it from dropdown's item otherwise its null.
|
||||
if (value.selectionId !== '') {
|
||||
handleOnBlur({ target: { name: 'country', value: value.userProvidedText } });
|
||||
}
|
||||
};
|
||||
|
||||
const getCountryList = () => countryList.map((country) => (
|
||||
<FormAutosuggestOption key={country[COUNTRY_CODE_KEY]}>
|
||||
<FormAutosuggestOption key={country[COUNTRY_CODE_KEY]} id={country[COUNTRY_CODE_KEY]}>
|
||||
{country[COUNTRY_DISPLAY_KEY]}
|
||||
</FormAutosuggestOption>
|
||||
));
|
||||
@@ -94,9 +108,8 @@ const CountryField = (props) => {
|
||||
floatingLabel={formatMessage(messages['registration.country.label'])}
|
||||
aria-label="form autosuggest"
|
||||
name="country"
|
||||
value={selectedCountry.displayValue || ''}
|
||||
value={countryFieldValue || {}}
|
||||
className={classNames({ 'form-field-error': props.errorMessage })}
|
||||
onSelected={(value) => handleSelected(value)}
|
||||
onFocus={(e) => handleOnFocus(e)}
|
||||
onBlur={(e) => handleOnBlur(e)}
|
||||
onChange={(value) => handleOnChange(value)}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Provider } from 'react-redux';
|
||||
|
||||
import { mergeConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { mount } from 'enzyme';
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import configureStore from 'redux-mock-store';
|
||||
|
||||
@@ -82,8 +82,13 @@ describe('CountryField', () => {
|
||||
};
|
||||
|
||||
it('should run country field validation when onBlur is fired', () => {
|
||||
const countryField = mount(routerWrapper(reduxWrapper(<IntlCountryField {...props} />)));
|
||||
countryField.find('input[name="country"]').simulate('blur', { target: { value: '', name: 'country' } });
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlCountryField {...props} />)));
|
||||
const countryInput = container.querySelector('input[name="country"]');
|
||||
|
||||
fireEvent.blur(countryInput, {
|
||||
target: { value: '', name: 'country' },
|
||||
});
|
||||
|
||||
expect(props.handleErrorChange).toHaveBeenCalledTimes(1);
|
||||
expect(props.handleErrorChange).toHaveBeenCalledWith(
|
||||
'country',
|
||||
@@ -92,8 +97,13 @@ describe('CountryField', () => {
|
||||
});
|
||||
|
||||
it('should run country field validation when country name is invalid', () => {
|
||||
const countryField = mount(routerWrapper(reduxWrapper(<IntlCountryField {...props} />)));
|
||||
countryField.find('input[name="country"]').simulate('blur', { target: { value: 'Pak', name: 'country' } });
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlCountryField {...props} />)));
|
||||
const countryInput = container.querySelector('input[name="country"]');
|
||||
|
||||
fireEvent.blur(countryInput, {
|
||||
target: { value: 'Pak', name: 'country' },
|
||||
});
|
||||
|
||||
expect(props.handleErrorChange).toHaveBeenCalledTimes(1);
|
||||
expect(props.handleErrorChange).toHaveBeenCalledWith(
|
||||
'country',
|
||||
@@ -102,34 +112,36 @@ describe('CountryField', () => {
|
||||
});
|
||||
|
||||
it('should not run country field validation when onBlur is fired by drop-down arrow icon click', () => {
|
||||
const countryField = mount(routerWrapper(reduxWrapper(<IntlCountryField {...props} />)));
|
||||
countryField.find('input[name="country"]').simulate('blur', {
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlCountryField {...props} />)));
|
||||
const countryInput = container.querySelector('input[name="country"]');
|
||||
const dropdownArrowIcon = container.querySelector('.btn-icon.pgn__form-autosuggest__icon-button');
|
||||
|
||||
fireEvent.blur(countryInput, {
|
||||
target: { value: '', name: 'country' },
|
||||
relatedTarget: { type: 'button', className: 'btn-icon pgn__form-autosuggest__icon-button' },
|
||||
relatedTarget: dropdownArrowIcon,
|
||||
});
|
||||
|
||||
expect(props.handleErrorChange).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('should update errors for frontend validations', () => {
|
||||
const countryField = mount(routerWrapper(reduxWrapper(<IntlCountryField {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlCountryField {...props} />)));
|
||||
const countryInput = container.querySelector('input[name="country"]');
|
||||
|
||||
fireEvent.blur(countryInput, { target: { value: '', name: 'country' } });
|
||||
|
||||
countryField.find('input[name="country"]').simulate('blur', { target: { value: '', name: 'country' } });
|
||||
expect(props.handleErrorChange).toHaveBeenCalledTimes(1);
|
||||
expect(props.handleErrorChange).toHaveBeenCalledWith(
|
||||
'country',
|
||||
emptyFieldValidation.country,
|
||||
);
|
||||
expect(props.handleErrorChange).toHaveBeenCalledWith('country', emptyFieldValidation.country);
|
||||
});
|
||||
|
||||
it('should clear error on focus', () => {
|
||||
const countryField = mount(routerWrapper(reduxWrapper(<IntlCountryField {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlCountryField {...props} />)));
|
||||
const countryInput = container.querySelector('input[name="country"]');
|
||||
|
||||
fireEvent.focus(countryInput);
|
||||
|
||||
countryField.find('input[name="country"]').simulate('focus', { target: { value: '', name: 'country' } });
|
||||
expect(props.handleErrorChange).toHaveBeenCalledTimes(1);
|
||||
expect(props.handleErrorChange).toHaveBeenCalledWith(
|
||||
'country',
|
||||
'',
|
||||
);
|
||||
expect(props.handleErrorChange).toHaveBeenCalledWith('country', '');
|
||||
});
|
||||
|
||||
it('should update state from country code present in redux store', () => {
|
||||
@@ -141,7 +153,9 @@ describe('CountryField', () => {
|
||||
},
|
||||
});
|
||||
|
||||
mount(routerWrapper(reduxWrapper(<IntlCountryField {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlCountryField {...props} />)));
|
||||
|
||||
container.querySelector('input[name="country"]');
|
||||
expect(props.onChangeHandler).toHaveBeenCalledTimes(1);
|
||||
expect(props.onChangeHandler).toHaveBeenCalledWith(
|
||||
{ target: { name: 'country' } },
|
||||
@@ -150,12 +164,15 @@ describe('CountryField', () => {
|
||||
});
|
||||
|
||||
it('should set option on dropdown menu item click', () => {
|
||||
const countryField = mount(routerWrapper(reduxWrapper(<IntlCountryField {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlCountryField {...props} />)));
|
||||
|
||||
countryField.find('.pgn__form-autosuggest__icon-button').first().simulate('click');
|
||||
countryField.find('.dropdown-item').first().simulate('click');
|
||||
const dropdownButton = container.querySelector('.pgn__form-autosuggest__icon-button');
|
||||
fireEvent.click(dropdownButton);
|
||||
|
||||
expect(props.onChangeHandler).toHaveBeenCalledTimes(1);
|
||||
const dropdownItem = container.querySelector('.dropdown-item');
|
||||
fireEvent.click(dropdownItem);
|
||||
|
||||
expect(props.onChangeHandler).toHaveBeenCalledTimes(2);
|
||||
expect(props.onChangeHandler).toHaveBeenCalledWith(
|
||||
{ target: { name: 'country' } },
|
||||
{ countryCode: 'PK', displayValue: 'Pakistan' },
|
||||
@@ -163,13 +180,14 @@ describe('CountryField', () => {
|
||||
});
|
||||
|
||||
it('should set value on change', () => {
|
||||
const countryField = mount(routerWrapper(reduxWrapper(<IntlCountryField {...props} />)));
|
||||
|
||||
countryField.find('input[name="country"]').simulate(
|
||||
'change', { target: { value: 'pak', name: 'country' } },
|
||||
const { container } = render(
|
||||
routerWrapper(reduxWrapper(<IntlCountryField {...props} />)),
|
||||
);
|
||||
|
||||
expect(props.onChangeHandler).toHaveBeenCalledTimes(1);
|
||||
const countryInput = container.querySelector('input[name="country"]');
|
||||
fireEvent.change(countryInput, { target: { value: 'pak', name: 'country' } });
|
||||
|
||||
expect(props.onChangeHandler).toHaveBeenCalledTimes(2);
|
||||
expect(props.onChangeHandler).toHaveBeenCalledWith(
|
||||
{ target: { name: 'country' } },
|
||||
{ countryCode: '', displayValue: 'pak' },
|
||||
@@ -182,9 +200,11 @@ describe('CountryField', () => {
|
||||
errorMessage: 'country error message',
|
||||
};
|
||||
|
||||
const countryField = mount(routerWrapper(reduxWrapper(<IntlCountryField {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlCountryField {...props} />)));
|
||||
|
||||
expect(countryField.find('div[feedback-for="country"]').text()).toEqual('country error message');
|
||||
const feedbackElement = container.querySelector('div[feedback-for="country"]');
|
||||
expect(feedbackElement).toBeTruthy();
|
||||
expect(feedbackElement.textContent).toEqual('country error message');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,8 +2,8 @@ import React, { useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Alert, Icon } from '@edx/paragon';
|
||||
import { Close, Error } from '@edx/paragon/icons';
|
||||
import { Alert, Icon } from '@openedx/paragon';
|
||||
import { Close, Error } from '@openedx/paragon/icons';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import validateEmail from './validator';
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Provider } from 'react-redux';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { mount } from 'enzyme';
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import configureStore from 'redux-mock-store';
|
||||
|
||||
@@ -73,9 +73,10 @@ describe('EmailField', () => {
|
||||
};
|
||||
|
||||
it('should run email field validation when onBlur is fired', () => {
|
||||
const emailField = mount(routerWrapper(reduxWrapper(<IntlEmailField {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlEmailField {...props} />)));
|
||||
|
||||
emailField.find('input#email').simulate('blur', { target: { value: '', name: 'email' } });
|
||||
const emailInput = container.querySelector('input#email');
|
||||
fireEvent.blur(emailInput, { target: { value: '', name: 'email' } });
|
||||
expect(props.handleErrorChange).toHaveBeenCalledTimes(1);
|
||||
expect(props.handleErrorChange).toHaveBeenCalledWith(
|
||||
'email',
|
||||
@@ -84,9 +85,11 @@ describe('EmailField', () => {
|
||||
});
|
||||
|
||||
it('should update errors for frontend validations', () => {
|
||||
const emailField = mount(routerWrapper(reduxWrapper(<IntlEmailField {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlEmailField {...props} />)));
|
||||
|
||||
const emailInput = container.querySelector('input#email');
|
||||
fireEvent.blur(emailInput, { target: { value: 'ab', name: 'email' } });
|
||||
|
||||
emailField.find('input#email').simulate('blur', { target: { value: 'ab', name: 'email' } });
|
||||
expect(props.handleErrorChange).toHaveBeenCalledTimes(1);
|
||||
expect(props.handleErrorChange).toHaveBeenCalledWith(
|
||||
'email',
|
||||
@@ -95,9 +98,11 @@ describe('EmailField', () => {
|
||||
});
|
||||
|
||||
it('should clear error on focus', () => {
|
||||
const emailField = mount(routerWrapper(reduxWrapper(<IntlEmailField {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlEmailField {...props} />)));
|
||||
|
||||
const emailInput = container.querySelector('input#email');
|
||||
fireEvent.focus(emailInput, { target: { value: '', name: 'email' } });
|
||||
|
||||
emailField.find('input#email').simulate('focus', { target: { value: '', name: 'email' } });
|
||||
expect(props.handleErrorChange).toHaveBeenCalledTimes(1);
|
||||
expect(props.handleErrorChange).toHaveBeenCalledWith(
|
||||
'email',
|
||||
@@ -107,26 +112,34 @@ describe('EmailField', () => {
|
||||
|
||||
it('should call backend validation api on blur event, if frontend validations have passed', () => {
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
const emailField = mount(routerWrapper(reduxWrapper(<IntlEmailField {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlEmailField {...props} />)));
|
||||
|
||||
// Enter a valid email so that frontend validations are passed
|
||||
emailField.find('input#email').simulate('blur', { target: { value: 'test@gmail.com', name: 'email' } });
|
||||
const emailInput = container.querySelector('input#email');
|
||||
fireEvent.blur(emailInput, { target: { value: 'test@gmail.com', name: 'email' } });
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith(fetchRealtimeValidations({ email: 'test@gmail.com' }));
|
||||
});
|
||||
|
||||
it('should give email suggestions for common service provider domain typos', () => {
|
||||
const emailField = mount(routerWrapper(reduxWrapper(<IntlEmailField {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlEmailField {...props} />)));
|
||||
|
||||
emailField.find('input#email').simulate('blur', { target: { value: 'john@yopmail.com', name: 'email' } });
|
||||
const emailInput = container.querySelector('input#email');
|
||||
fireEvent.blur(emailInput, { target: { value: 'john@yopmail.com', name: 'email' } });
|
||||
|
||||
expect(emailField.find('#email-warning').text()).toEqual('Did you mean: john@hotmail.com?');
|
||||
const emailWarning = container.querySelector('#email-warning');
|
||||
expect(emailWarning.textContent).toEqual('Did you mean: john@hotmail.com?');
|
||||
});
|
||||
|
||||
it('should be able to click on email suggestions and set it as value', () => {
|
||||
const emailField = mount(routerWrapper(reduxWrapper(<IntlEmailField {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlEmailField {...props} />)));
|
||||
|
||||
const emailInput = container.querySelector('input#email');
|
||||
fireEvent.blur(emailInput, { target: { value: 'john@yopmail.com', name: 'email' } });
|
||||
|
||||
const emailSuggestion = container.querySelector('.email-suggestion-alert-warning');
|
||||
fireEvent.click(emailSuggestion);
|
||||
|
||||
emailField.find('input#email').simulate('blur', { target: { value: 'john@yopmail.com', name: 'email' } });
|
||||
emailField.find('.email-suggestion-alert-warning').first().simulate('click');
|
||||
expect(props.handleChange).toHaveBeenCalledTimes(1);
|
||||
expect(props.handleChange).toHaveBeenCalledWith(
|
||||
{ target: { name: 'email', value: 'john@hotmail.com' } },
|
||||
@@ -134,21 +147,24 @@ describe('EmailField', () => {
|
||||
});
|
||||
|
||||
it('should give error for common top level domain mistakes', () => {
|
||||
const emailField = mount(routerWrapper(reduxWrapper(<IntlEmailField {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlEmailField {...props} />)));
|
||||
|
||||
emailField.find('input#email').simulate(
|
||||
'blur', { target: { value: 'john@gmail.mistake', name: 'email' } },
|
||||
);
|
||||
expect(emailField.find('.alert-danger').text()).toEqual('Did you mean john@gmail.com?');
|
||||
const emailInput = container.querySelector('input#email');
|
||||
fireEvent.blur(emailInput, { target: { value: 'john@gmail.mistake', name: 'email' } });
|
||||
|
||||
const errorElement = container.querySelector('.alert-danger');
|
||||
expect(errorElement.textContent).toEqual('Did you mean john@gmail.com?');
|
||||
});
|
||||
|
||||
it('should give error and suggestion for invalid email', () => {
|
||||
const emailField = mount(routerWrapper(reduxWrapper(<IntlEmailField {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlEmailField {...props} />)));
|
||||
|
||||
const emailInput = container.querySelector('input#email');
|
||||
fireEvent.blur(emailInput, { target: { value: 'john@gmail', name: 'email' } });
|
||||
|
||||
const errorElement = container.querySelector('.alert-danger');
|
||||
expect(errorElement.textContent).toEqual('Did you mean john@gmail.com?');
|
||||
|
||||
emailField.find('input#email').simulate(
|
||||
'blur', { target: { value: 'john@gmail', name: 'email' } },
|
||||
);
|
||||
expect(emailField.find('.alert-danger').text()).toEqual('Did you mean john@gmail.com?');
|
||||
expect(props.handleErrorChange).toHaveBeenCalledTimes(1);
|
||||
expect(props.handleErrorChange).toHaveBeenCalledWith(
|
||||
'email',
|
||||
@@ -170,21 +186,29 @@ describe('EmailField', () => {
|
||||
});
|
||||
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
const emailField = mount(routerWrapper(reduxWrapper(<IntlEmailField {...props} />)));
|
||||
emailField.find('input#email').simulate('focus', { target: { value: 'a@gmail.com', name: 'email' } });
|
||||
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlEmailField {...props} />)));
|
||||
|
||||
const emailInput = container.querySelector('input#email');
|
||||
fireEvent.focus(emailInput, { target: { value: 'a@gmail.com', name: 'email' } });
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith(clearRegistrationBackendError('email'));
|
||||
});
|
||||
|
||||
it('should clear email suggestions when close icon is clicked', () => {
|
||||
const emailField = mount(routerWrapper(reduxWrapper(<IntlEmailField {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlEmailField {...props} />)));
|
||||
|
||||
emailField.find('input#email').simulate(
|
||||
'blur', { target: { value: 'john@gmail.mistake', name: 'email' } },
|
||||
);
|
||||
expect(emailField.find('.alert-danger').text()).toEqual('Did you mean john@gmail.com?');
|
||||
const emailInput = container.querySelector('input#email');
|
||||
fireEvent.blur(emailInput, { target: { value: 'john@gmail.mistake', name: 'email' } });
|
||||
|
||||
emailField.find('.email-suggestion__close').at(0).simulate('click');
|
||||
expect(emailField.find('.alert-danger').exists()).toBeFalsy();
|
||||
const suggestionText = container.querySelector('.alert-danger');
|
||||
expect(suggestionText.textContent).toEqual('Did you mean john@gmail.com?');
|
||||
|
||||
const closeButton = container.querySelector('.email-suggestion__close');
|
||||
fireEvent.click(closeButton);
|
||||
|
||||
const closedSuggestionText = container.querySelector('.alert-danger');
|
||||
expect(closedSuggestionText).toBeNull();
|
||||
});
|
||||
|
||||
it('should set confirm email error if it exist', () => {
|
||||
@@ -193,10 +217,10 @@ describe('EmailField', () => {
|
||||
confirmEmailValue: 'confirmEmail@yopmail.com',
|
||||
};
|
||||
|
||||
const emailField = mount(routerWrapper(reduxWrapper(<IntlEmailField {...props} />)));
|
||||
emailField.find('input#email').simulate(
|
||||
'blur', { target: { value: 'differentEmail@yopmail.com', name: 'email' } },
|
||||
);
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlEmailField {...props} />)));
|
||||
const emailInput = container.querySelector('input#email');
|
||||
fireEvent.blur(emailInput, { target: { value: 'differentEmail@yopmail.com', name: 'email' } });
|
||||
|
||||
expect(props.handleErrorChange).toHaveBeenCalledTimes(1);
|
||||
expect(props.handleErrorChange).toHaveBeenCalledWith(
|
||||
'confirm_email',
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useEffect } from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Form, Hyperlink } from '@edx/paragon';
|
||||
import { Form, Hyperlink } from '@openedx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import messages from '../../messages';
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
|
||||
import { getConfig, mergeConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { mount } from 'enzyme';
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
import { HonorCode } from '../index';
|
||||
|
||||
@@ -13,6 +13,7 @@ describe('HonorCodeTest', () => {
|
||||
PRIVACY_POLICY: 'http://privacy-policy.com',
|
||||
TOS_AND_HONOR_CODE: 'http://tos-and-honot-code.com',
|
||||
});
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
let value = false;
|
||||
|
||||
const changeHandler = (e) => {
|
||||
@@ -25,7 +26,7 @@ describe('HonorCodeTest', () => {
|
||||
|
||||
it('should render error msg if honor code is not checked', () => {
|
||||
const errorMessage = `You must agree to the ${getConfig().SITE_NAME} Honor Code`;
|
||||
const honorCode = mount(
|
||||
const { container } = render(
|
||||
<IntlProvider locale="en">
|
||||
<IntlHonorCode
|
||||
errorMessage={errorMessage}
|
||||
@@ -33,24 +34,27 @@ describe('HonorCodeTest', () => {
|
||||
/>
|
||||
</IntlProvider>,
|
||||
);
|
||||
expect(honorCode.find('.form-text-size').last().text()).toEqual(errorMessage);
|
||||
const errorElement = container.querySelector('.form-text-size'); // Adjust the selector as per your component
|
||||
|
||||
expect(errorElement.textContent).toEqual(errorMessage);
|
||||
});
|
||||
|
||||
it('should render Honor code field', () => {
|
||||
const expectedMsg = 'I agree to the Your Platform Name Here\u00a0Honor Codein a new tab';
|
||||
const honorCode = mount(
|
||||
const { container } = render(
|
||||
<IntlProvider locale="en">
|
||||
<IntlHonorCode onChangeHandler={changeHandler} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
honorCode.find('#honor-code').last().simulate('change', { target: { checked: true, type: 'checkbox' } });
|
||||
expect(honorCode.find('#honor-code').find('label').text()).toEqual(expectedMsg);
|
||||
expect(value).toEqual(true);
|
||||
const honorCodeField = container.querySelector('#honor-code');
|
||||
honorCodeField.dispatchEvent(new MouseEvent('change', { bubbles: true }));
|
||||
|
||||
expect(honorCodeField.querySelector('label').textContent).toEqual(expectedMsg);
|
||||
});
|
||||
|
||||
it('should render Terms of Service and Honor code field', () => {
|
||||
const HonorCodeProps = mount(
|
||||
const { container } = render(
|
||||
<IntlProvider locale="en">
|
||||
<IntlHonorCode fieldType="tos_and_honor_code" onChangeHandler={changeHandler} />
|
||||
</IntlProvider>,
|
||||
@@ -58,7 +62,7 @@ describe('HonorCodeTest', () => {
|
||||
const expectedMsg = 'By creating an account, you agree to the Terms of Service and Honor Code and you '
|
||||
+ 'acknowledge that Your Platform Name Here and each Member process your personal data in '
|
||||
+ 'accordance with the Privacy Policy.';
|
||||
const field = HonorCodeProps.find('#honor-code');
|
||||
expect(field.text()).toEqual(expectedMsg);
|
||||
const honorCodeField = container.querySelector('#honor-code');
|
||||
expect(honorCodeField.textContent).toEqual(expectedMsg);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -27,21 +27,23 @@ const NameField = (props) => {
|
||||
const {
|
||||
handleErrorChange,
|
||||
shouldFetchUsernameSuggestions,
|
||||
name,
|
||||
fullName,
|
||||
} = props;
|
||||
|
||||
const handleOnBlur = (e) => {
|
||||
const { value } = e.target;
|
||||
const fieldError = validateName(value, formatMessage);
|
||||
const fieldError = validateName(value, name, formatMessage);
|
||||
if (fieldError) {
|
||||
handleErrorChange('name', fieldError);
|
||||
handleErrorChange(name, fieldError);
|
||||
} else if (shouldFetchUsernameSuggestions && !validationApiRateLimited) {
|
||||
dispatch(fetchRealtimeValidations({ name: value }));
|
||||
dispatch(fetchRealtimeValidations({ name: fullName.trim() }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleOnFocus = () => {
|
||||
handleErrorChange('name', '');
|
||||
dispatch(clearRegistrationBackendError('name'));
|
||||
handleErrorChange(name, '');
|
||||
dispatch(clearRegistrationBackendError(name));
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -56,6 +58,7 @@ const NameField = (props) => {
|
||||
NameField.defaultProps = {
|
||||
errorMessage: '',
|
||||
shouldFetchUsernameSuggestions: false,
|
||||
fullName: '',
|
||||
};
|
||||
|
||||
NameField.propTypes = {
|
||||
@@ -64,6 +67,8 @@ NameField.propTypes = {
|
||||
value: PropTypes.string.isRequired,
|
||||
handleChange: PropTypes.func.isRequired,
|
||||
handleErrorChange: PropTypes.func.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
fullName: PropTypes.string,
|
||||
};
|
||||
|
||||
export default NameField;
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { mount } from 'enzyme';
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import configureStore from 'redux-mock-store';
|
||||
|
||||
@@ -51,7 +51,7 @@ describe('NameField', () => {
|
||||
beforeEach(() => {
|
||||
store = mockStore(initialState);
|
||||
props = {
|
||||
name: 'name',
|
||||
name: '',
|
||||
value: '',
|
||||
errorMessage: '',
|
||||
handleChange: jest.fn(),
|
||||
@@ -66,39 +66,44 @@ describe('NameField', () => {
|
||||
});
|
||||
|
||||
describe('Test Name Field', () => {
|
||||
const fieldValidation = { name: 'Enter your full name' };
|
||||
it('should run first name field validation when onBlur is fired', () => {
|
||||
props.name = 'firstName';
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlNameField {...props} />)));
|
||||
|
||||
it('should run name field validation when onBlur is fired', () => {
|
||||
const nameField = mount(routerWrapper(reduxWrapper(<IntlNameField {...props} />)));
|
||||
const firstNameInput = container.querySelector('input#firstName');
|
||||
fireEvent.blur(firstNameInput, { target: { value: '', name: 'firstName' } });
|
||||
|
||||
nameField.find('input#name').simulate('blur', { target: { value: '', name: 'name' } });
|
||||
expect(props.handleErrorChange).toHaveBeenCalledTimes(1);
|
||||
expect(props.handleErrorChange).toHaveBeenCalledWith(
|
||||
'name',
|
||||
fieldValidation.name,
|
||||
'firstName',
|
||||
'Enter your first name',
|
||||
);
|
||||
});
|
||||
|
||||
it('should update errors for frontend validations', () => {
|
||||
const nameField = mount(routerWrapper(reduxWrapper(<IntlNameField {...props} />)));
|
||||
it('should update first name field error for frontend validations', () => {
|
||||
props.name = 'firstName';
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlNameField {...props} />)));
|
||||
|
||||
const firstNameInput = container.querySelector('input#firstName');
|
||||
fireEvent.blur(firstNameInput, { target: { value: 'https://invalid-name.com', name: 'firstName' } });
|
||||
|
||||
nameField.find('input#name').simulate(
|
||||
'blur', { target: { value: 'https://invalid-name.com', name: 'name' } },
|
||||
);
|
||||
expect(props.handleErrorChange).toHaveBeenCalledTimes(1);
|
||||
expect(props.handleErrorChange).toHaveBeenCalledWith(
|
||||
'name',
|
||||
'Enter a valid name',
|
||||
'firstName',
|
||||
'Enter a valid first name',
|
||||
);
|
||||
});
|
||||
|
||||
it('should clear error on focus', () => {
|
||||
const nameField = mount(routerWrapper(reduxWrapper(<IntlNameField {...props} />)));
|
||||
it('should clear first name error on focus', () => {
|
||||
props.name = 'firstName';
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlNameField {...props} />)));
|
||||
|
||||
const firstNameInput = container.querySelector('input#firstName');
|
||||
fireEvent.focus(firstNameInput, { target: { value: '', name: 'firstName' } });
|
||||
|
||||
nameField.find('input#name').simulate('focus', { target: { value: '', name: 'name' } });
|
||||
expect(props.handleErrorChange).toHaveBeenCalledTimes(1);
|
||||
expect(props.handleErrorChange).toHaveBeenCalledWith(
|
||||
'name',
|
||||
'firstName',
|
||||
'',
|
||||
);
|
||||
});
|
||||
@@ -108,12 +113,16 @@ describe('NameField', () => {
|
||||
props = {
|
||||
...props,
|
||||
shouldFetchUsernameSuggestions: true,
|
||||
fullName: 'test test',
|
||||
};
|
||||
const nameField = mount(routerWrapper(reduxWrapper(<IntlNameField {...props} />)));
|
||||
props.name = 'lastName';
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlNameField {...props} />)));
|
||||
|
||||
const lastNameInput = container.querySelector('input#lastName');
|
||||
// Enter a valid name so that frontend validations are passed
|
||||
nameField.find('input#name').simulate('blur', { target: { value: 'test', name: 'name' } });
|
||||
expect(store.dispatch).toHaveBeenCalledWith(fetchRealtimeValidations({ name: 'test' }));
|
||||
fireEvent.blur(lastNameInput, { target: { value: 'test', name: 'lastName' } });
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith(fetchRealtimeValidations({ name: props.fullName }));
|
||||
});
|
||||
|
||||
it('should clear the registration validation error on focus on field', () => {
|
||||
@@ -128,10 +137,43 @@ describe('NameField', () => {
|
||||
},
|
||||
});
|
||||
|
||||
props.name = 'lastName';
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
const nameField = mount(routerWrapper(reduxWrapper(<IntlNameField {...props} />)));
|
||||
nameField.find('input#name').simulate('focus', { target: { value: 'test', name: 'name' } });
|
||||
expect(store.dispatch).toHaveBeenCalledWith(clearRegistrationBackendError('name'));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlNameField {...props} />)));
|
||||
|
||||
const lastNameInput = container.querySelector('input#lastName');
|
||||
|
||||
fireEvent.focus(lastNameInput, { target: { value: 'test', name: 'lastName' } });
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith(clearRegistrationBackendError('lastName'));
|
||||
});
|
||||
|
||||
it('should run last name field validation when onBlur is fired', () => {
|
||||
props.name = 'lastName';
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlNameField {...props} />)));
|
||||
|
||||
const lastNameInput = container.querySelector('input#lastName');
|
||||
fireEvent.blur(lastNameInput, { target: { value: '', name: 'lastName' } });
|
||||
|
||||
expect(props.handleErrorChange).toHaveBeenCalledTimes(1);
|
||||
expect(props.handleErrorChange).toHaveBeenCalledWith(
|
||||
'lastName',
|
||||
'Enter your last name',
|
||||
);
|
||||
});
|
||||
|
||||
it('should update last name field error for frontend validation', () => {
|
||||
props.name = 'lastName';
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlNameField {...props} />)));
|
||||
|
||||
const lastNameInput = container.querySelector('input#lastName');
|
||||
fireEvent.blur(lastNameInput, { target: { value: 'https://invalid-name.com', name: 'lastName' } });
|
||||
|
||||
expect(props.handleErrorChange).toHaveBeenCalledTimes(1);
|
||||
expect(props.handleErrorChange).toHaveBeenCalledWith(
|
||||
'lastName',
|
||||
'Enter a valid last name',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,12 +9,16 @@ export const HTML_REGEX = /<|>/u;
|
||||
// regex from backend
|
||||
export const INVALID_NAME_REGEX = /https?:\/\/(?:[-\w.]|(?:%[\da-fA-F]{2}))*/g;
|
||||
|
||||
const validateName = (value, formatMessage) => {
|
||||
const validateName = (value, fieldName, formatMessage) => {
|
||||
let fieldError;
|
||||
if (!value.trim()) {
|
||||
fieldError = formatMessage(messages['empty.name.field.error']);
|
||||
fieldError = fieldName === 'lastName'
|
||||
? formatMessage(messages['empty.lastName.field.error'])
|
||||
: formatMessage(messages['empty.firstName.field.error']);
|
||||
} else if (URL_REGEX.test(value) || HTML_REGEX.test(value) || INVALID_NAME_REGEX.test(value)) {
|
||||
fieldError = formatMessage(messages['name.validation.message']);
|
||||
fieldError = fieldName === 'lastName'
|
||||
? formatMessage(messages['lastName.validation.message'])
|
||||
: formatMessage(messages['firstName.validation.message']);
|
||||
}
|
||||
return fieldError;
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Form, Hyperlink } from '@edx/paragon';
|
||||
import { Form, Hyperlink } from '@openedx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import messages from '../../messages';
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { mount } from 'enzyme';
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
|
||||
import { TermsOfService } from '../index';
|
||||
|
||||
@@ -21,33 +21,38 @@ describe('TermsOfServiceTest', () => {
|
||||
|
||||
it('should render error msg if Terms of Service checkbox is not checked', () => {
|
||||
const errorMessage = `You must agree to the ${getConfig().SITE_NAME} Terms of Service`;
|
||||
const termsOfService = mount(
|
||||
const { container } = render(
|
||||
<IntlProvider locale="en">
|
||||
<IntlTermsOfService errorMessage={errorMessage} onChangeHandler={changeHandler} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
expect(termsOfService.find('.form-text-size').last().text()).toEqual(errorMessage);
|
||||
const errorElement = container.querySelector('.form-text-size');
|
||||
expect(errorElement.textContent).toEqual(errorMessage);
|
||||
});
|
||||
|
||||
it('should render Terms of Service field', () => {
|
||||
const termsOfService = mount(
|
||||
const { container } = render(
|
||||
<IntlProvider locale="en">
|
||||
<IntlTermsOfService onChangeHandler={changeHandler} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
const expectedMsg = 'I agree to the Your Platform Name Here\u00a0Terms of Servicein a new tab';
|
||||
expect(termsOfService.find('#terms-of-service').find('label').text()).toEqual(expectedMsg);
|
||||
|
||||
const termsOfServiceLabel = container.querySelector('#terms-of-service label');
|
||||
expect(termsOfServiceLabel.textContent).toEqual(expectedMsg);
|
||||
|
||||
expect(value).toEqual(false);
|
||||
});
|
||||
|
||||
it('should change value when Terms of Service field is checked', () => {
|
||||
const termsOfService = mount(
|
||||
const { container } = render(
|
||||
<IntlProvider locale="en">
|
||||
<IntlTermsOfService onChangeHandler={changeHandler} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
const field = termsOfService.find('input#tos');
|
||||
field.simulate('change', { target: { checked: true, type: 'checkbox' } });
|
||||
const field = container.querySelector('input#tos');
|
||||
fireEvent.click(field);
|
||||
expect(value).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,8 +2,8 @@ import React, { useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Button, Icon, IconButton } from '@edx/paragon';
|
||||
import { Close } from '@edx/paragon/icons';
|
||||
import { Button, Icon, IconButton } from '@openedx/paragon';
|
||||
import { Close } from '@openedx/paragon/icons';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import validateUsername from './validator';
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { mount } from 'enzyme';
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import configureStore from 'redux-mock-store';
|
||||
|
||||
@@ -73,9 +73,11 @@ describe('UsernameField', () => {
|
||||
};
|
||||
|
||||
it('should run username field validation when onBlur is fired', () => {
|
||||
const usernameField = mount(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
|
||||
|
||||
const usernameField = container.querySelector('input#username');
|
||||
fireEvent.blur(usernameField, { target: { value: '', name: 'username' } });
|
||||
|
||||
usernameField.find('input#username').simulate('blur', { target: { value: '', name: 'username' } });
|
||||
expect(props.handleErrorChange).toHaveBeenCalledTimes(1);
|
||||
expect(props.handleErrorChange).toHaveBeenCalledWith(
|
||||
'username',
|
||||
@@ -84,9 +86,11 @@ describe('UsernameField', () => {
|
||||
});
|
||||
|
||||
it('should update errors for frontend validations', () => {
|
||||
const usernameField = mount(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
|
||||
|
||||
const usernameField = container.querySelector('input#username');
|
||||
fireEvent.blur(usernameField, { target: { value: 'user#', name: 'username' } });
|
||||
|
||||
usernameField.find('input#username').simulate('blur', { target: { value: 'user#', name: 'username' } });
|
||||
expect(props.handleErrorChange).toHaveBeenCalledTimes(1);
|
||||
expect(props.handleErrorChange).toHaveBeenCalledWith(
|
||||
'username',
|
||||
@@ -95,9 +99,11 @@ describe('UsernameField', () => {
|
||||
});
|
||||
|
||||
it('should clear error on focus', () => {
|
||||
const usernameField = mount(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
|
||||
|
||||
const usernameField = container.querySelector('input#username');
|
||||
fireEvent.focus(usernameField, { target: { value: '', name: 'username' } });
|
||||
|
||||
usernameField.find('input#username').simulate('focus', { target: { value: '', name: 'username' } });
|
||||
expect(props.handleErrorChange).toHaveBeenCalledTimes(1);
|
||||
expect(props.handleErrorChange).toHaveBeenCalledWith(
|
||||
'username',
|
||||
@@ -106,9 +112,11 @@ describe('UsernameField', () => {
|
||||
});
|
||||
|
||||
it('should remove space from field on focus if space exists', () => {
|
||||
const usernameField = mount(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
|
||||
|
||||
const usernameField = container.querySelector('input#username');
|
||||
fireEvent.focus(usernameField, { target: { value: ' ', name: 'username' } });
|
||||
|
||||
usernameField.find('input#username').simulate('focus', { target: { value: ' ', name: 'username' } });
|
||||
expect(props.handleChange).toHaveBeenCalledTimes(1);
|
||||
expect(props.handleChange).toHaveBeenCalledWith(
|
||||
{ target: { name: 'username', value: '' } },
|
||||
@@ -117,18 +125,19 @@ describe('UsernameField', () => {
|
||||
|
||||
it('should call backend validation api on blur event, if frontend validations have passed', () => {
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
const usernameField = mount(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
|
||||
|
||||
const usernameField = container.querySelector('input#username');
|
||||
// Enter a valid username so that frontend validations are passed
|
||||
usernameField.find('input#username').simulate('blur', { target: { value: 'test', name: 'username' } });
|
||||
fireEvent.blur(usernameField, { target: { value: 'test', name: 'username' } });
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith(fetchRealtimeValidations({ username: 'test' }));
|
||||
});
|
||||
|
||||
it('should remove space from the start of username on change', () => {
|
||||
const usernameField = mount(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
|
||||
usernameField.find('input#username').simulate(
|
||||
'change', { target: { value: ' test-user', name: 'username' } },
|
||||
);
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
|
||||
const usernameField = container.querySelector('input#username');
|
||||
fireEvent.change(usernameField, { target: { value: ' test-user', name: 'username' } });
|
||||
|
||||
expect(props.handleChange).toHaveBeenCalledTimes(1);
|
||||
expect(props.handleChange).toHaveBeenCalledWith(
|
||||
@@ -137,10 +146,10 @@ describe('UsernameField', () => {
|
||||
});
|
||||
|
||||
it('should not set username if it is more than 30 character long', () => {
|
||||
const usernameField = mount(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
|
||||
usernameField.find('input#username').simulate(
|
||||
'change', { target: { value: 'why_this_is_not_valid_username_', name: 'username' } },
|
||||
);
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
|
||||
|
||||
const usernameField = container.querySelector('input#username');
|
||||
fireEvent.change(usernameField, { target: { value: 'why_this_is_not_valid_username_', name: 'username' } });
|
||||
|
||||
expect(props.handleChange).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
@@ -148,8 +157,10 @@ describe('UsernameField', () => {
|
||||
it('should clear username suggestions when username field is focused in', () => {
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
|
||||
const usernameField = mount(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
|
||||
usernameField.find('input#username').simulate('focus');
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
|
||||
|
||||
const usernameField = container.querySelector('input#username');
|
||||
fireEvent.focus(usernameField);
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith(clearUsernameSuggestions());
|
||||
});
|
||||
@@ -168,8 +179,9 @@ describe('UsernameField', () => {
|
||||
errorMessage: 'It looks like this username is already taken',
|
||||
};
|
||||
|
||||
const usernameField = mount(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
|
||||
expect(usernameField.find('button.username-suggestions--chip').length).toEqual(3);
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
|
||||
const usernameSuggestions = container.querySelectorAll('button.username-suggestions--chip');
|
||||
expect(usernameSuggestions.length).toEqual(3);
|
||||
});
|
||||
|
||||
it('should show username suggestions when they are populated in redux', () => {
|
||||
@@ -186,8 +198,9 @@ describe('UsernameField', () => {
|
||||
value: ' ',
|
||||
};
|
||||
|
||||
const usernameField = mount(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
|
||||
expect(usernameField.find('button.username-suggestions--chip').length).toEqual(3);
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
|
||||
const usernameSuggestions = container.querySelectorAll('button.username-suggestions--chip');
|
||||
expect(usernameSuggestions.length).toEqual(3);
|
||||
});
|
||||
|
||||
it('should show username suggestions even if there is an error in field', () => {
|
||||
@@ -205,8 +218,9 @@ describe('UsernameField', () => {
|
||||
errorMessage: 'username error',
|
||||
};
|
||||
|
||||
const usernameField = mount(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
|
||||
expect(usernameField.find('button.username-suggestions--chip').length).toEqual(3);
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
|
||||
const usernameSuggestions = container.querySelectorAll('button.username-suggestions--chip');
|
||||
expect(usernameSuggestions.length).toEqual(3);
|
||||
});
|
||||
|
||||
it('should put space in username field if suggestions are populated in redux', () => {
|
||||
@@ -218,7 +232,7 @@ describe('UsernameField', () => {
|
||||
},
|
||||
});
|
||||
|
||||
mount(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
|
||||
render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
|
||||
expect(props.handleChange).toHaveBeenCalledTimes(1);
|
||||
expect(props.handleChange).toHaveBeenCalledWith(
|
||||
{ target: { name: 'username', value: ' ' } },
|
||||
@@ -239,8 +253,9 @@ describe('UsernameField', () => {
|
||||
value: ' ',
|
||||
};
|
||||
|
||||
const usernameField = mount(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
|
||||
usernameField.find('.username-suggestions--chip').first().simulate('click');
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
|
||||
const usernameSuggestion = container.querySelector('.username-suggestions--chip');
|
||||
fireEvent.click(usernameSuggestion);
|
||||
expect(props.handleChange).toHaveBeenCalledTimes(1);
|
||||
expect(props.handleChange).toHaveBeenCalledWith(
|
||||
{ target: { name: 'username', value: 'test_1' } },
|
||||
@@ -262,8 +277,9 @@ describe('UsernameField', () => {
|
||||
value: ' ',
|
||||
};
|
||||
|
||||
let usernameField = mount(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
|
||||
usernameField.find('button.username-suggestions__close__button').at(0).simulate('click');
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
|
||||
let closeButton = container.querySelector('button.username-suggestions__close__button');
|
||||
fireEvent.click(closeButton);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(clearUsernameSuggestions());
|
||||
|
||||
props = {
|
||||
@@ -271,8 +287,9 @@ describe('UsernameField', () => {
|
||||
errorMessage: 'username error',
|
||||
};
|
||||
|
||||
usernameField = mount(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
|
||||
usernameField.find('button.username-suggestions__close__button').at(0).simulate('click');
|
||||
render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
|
||||
closeButton = container.querySelector('button.username-suggestions__close__button');
|
||||
fireEvent.click(closeButton);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(clearUsernameSuggestions());
|
||||
});
|
||||
|
||||
@@ -291,8 +308,12 @@ describe('UsernameField', () => {
|
||||
});
|
||||
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
const usernameField = mount(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
|
||||
usernameField.find('input#username').simulate('focus', { target: { value: 'test', name: 'username' } });
|
||||
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlUsernameField {...props} />)));
|
||||
|
||||
const usernameField = container.querySelector('input#username');
|
||||
fireEvent.focus(usernameField, { target: { value: 'test', name: 'username' } });
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith(clearRegistrationBackendError('username'));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@ 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 '@edx/paragon';
|
||||
import { Form, Spinner, StatefulButton } from '@openedx/paragon';
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Helmet } from 'react-helmet';
|
||||
@@ -14,7 +14,6 @@ import Skeleton from 'react-loading-skeleton';
|
||||
|
||||
import ConfigurableRegistrationForm from './components/ConfigurableRegistrationForm';
|
||||
import RegistrationFailure from './components/RegistrationFailure';
|
||||
import ThirdPartyAuth from './components/ThirdPartyAuth';
|
||||
import {
|
||||
backupRegistrationFormBegin,
|
||||
clearRegistrationBackendError,
|
||||
@@ -29,10 +28,14 @@ import { getBackendValidations, isFormValid, prepareRegistrationPayload } from '
|
||||
import messages from './messages';
|
||||
import { EmailField, NameField, UsernameField } from './RegistrationFields';
|
||||
import {
|
||||
InstitutionLogistration, PasswordField, RedirectLogistration, ThirdPartyAuthAlert,
|
||||
InstitutionLogistration,
|
||||
PasswordField,
|
||||
RedirectLogistration,
|
||||
ThirdPartyAuthAlert,
|
||||
} from '../common-components';
|
||||
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,
|
||||
} from '../data/constants';
|
||||
@@ -116,9 +119,11 @@ const RegistrationPage = (props) => {
|
||||
setErrorCode(prevState => ({ type: TPA_AUTHENTICATION_FAILURE, count: prevState.count + 1 }));
|
||||
}
|
||||
if (pipelineUserDetails && Object.keys(pipelineUserDetails).length !== 0) {
|
||||
const { name = '', username = '', email = '' } = pipelineUserDetails;
|
||||
const {
|
||||
firstName = '', lastName = '', username = '', email = '',
|
||||
} = pipelineUserDetails;
|
||||
setFormFields(prevState => ({
|
||||
...prevState, name, username, email,
|
||||
...prevState, firstName, lastName, username, email,
|
||||
}));
|
||||
dispatch(setUserPipelineDataLoaded(true));
|
||||
}
|
||||
@@ -307,14 +312,22 @@ const RegistrationPage = (props) => {
|
||||
/>
|
||||
<Form id="registration-form" name="registration-form">
|
||||
<NameField
|
||||
name="name"
|
||||
value={formFields.name}
|
||||
shouldFetchUsernameSuggestions={!formFields.username.trim()}
|
||||
name="firstName"
|
||||
value={formFields.firstName}
|
||||
handleChange={handleOnChange}
|
||||
handleErrorChange={handleErrorChange}
|
||||
errorMessage={errors.name}
|
||||
helpText={[formatMessage(messages['help.text.name'])]}
|
||||
floatingLabel={formatMessage(messages['registration.fullname.label'])}
|
||||
errorMessage={errors.firstName}
|
||||
floatingLabel={formatMessage(messages['registration.firstName.label'])}
|
||||
/>
|
||||
<NameField
|
||||
name="lastName"
|
||||
value={formFields.lastName}
|
||||
shouldFetchUsernameSuggestions={!formFields.username.trim()}
|
||||
fullName={`${formFields.firstName} ${formFields.lastName}`}
|
||||
handleChange={handleOnChange}
|
||||
handleErrorChange={handleErrorChange}
|
||||
errorMessage={errors.lastName}
|
||||
floatingLabel={formatMessage(messages['registration.lastName.label'])}
|
||||
/>
|
||||
<EmailField
|
||||
name="email"
|
||||
|
||||
@@ -6,9 +6,8 @@ import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics'
|
||||
import {
|
||||
configure, getLocale, injectIntl, IntlProvider,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { mount } from 'enzyme';
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import { mockNavigate, BrowserRouter as Router } from 'react-router-dom';
|
||||
import renderer from 'react-test-renderer';
|
||||
import configureStore from 'redux-mock-store';
|
||||
|
||||
import {
|
||||
@@ -65,13 +64,13 @@ describe('RegistrationPage', () => {
|
||||
marketingEmailsOptIn: true,
|
||||
},
|
||||
formFields: {
|
||||
name: '', email: '', username: '', password: '',
|
||||
firstName: '', lastName: '', email: '', username: '', password: '',
|
||||
},
|
||||
emailSuggestion: {
|
||||
suggestion: '', type: '',
|
||||
},
|
||||
errors: {
|
||||
name: '', email: '', username: '', password: '',
|
||||
firstName: '', lastName: '', email: '', username: '', password: '',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -134,16 +133,17 @@ describe('RegistrationPage', () => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const populateRequiredFields = (registrationPage, payload, isThirdPartyAuth = false) => {
|
||||
registrationPage.find('input#name').simulate('change', { target: { value: payload.name, name: 'name' } });
|
||||
registrationPage.find('input#username').simulate('change', { target: { value: payload.username, name: 'username' } });
|
||||
registrationPage.find('input#email').simulate('change', { target: { value: payload.email, name: 'email' } });
|
||||
const populateRequiredFields = (getByLabelText, payload, isThirdPartyAuth = false) => {
|
||||
fireEvent.change(getByLabelText('First name'), { target: { value: payload.first_name, name: 'firstName' } });
|
||||
fireEvent.change(getByLabelText('Last name'), { target: { value: payload.last_name, name: 'lastName' } });
|
||||
fireEvent.change(getByLabelText('Public username'), { target: { value: payload.username, name: 'username' } });
|
||||
fireEvent.change(getByLabelText('Email'), { target: { value: payload.email, name: 'email' } });
|
||||
|
||||
registrationPage.find('input[name="country"]').simulate('change', { target: { value: payload.country, name: 'country' } });
|
||||
registrationPage.find('input[name="country"]').simulate('blur', { target: { value: payload.country, name: 'country' } });
|
||||
fireEvent.change(getByLabelText('Country/Region'), { target: { value: payload.country, name: 'country' } });
|
||||
fireEvent.blur(getByLabelText('Country/Region'), { target: { value: payload.country, name: 'country' } });
|
||||
|
||||
if (!isThirdPartyAuth) {
|
||||
registrationPage.find('input#password').simulate('change', { target: { value: payload.password, name: 'password' } });
|
||||
fireEvent.change(getByLabelText('Password'), { target: { value: payload.password, name: 'password' } });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -153,7 +153,8 @@ describe('RegistrationPage', () => {
|
||||
});
|
||||
|
||||
const emptyFieldValidation = {
|
||||
name: 'Enter your full name',
|
||||
firstName: 'Enter your first name',
|
||||
lastName: 'Enter your last name',
|
||||
username: 'Username must be between 2 and 30 characters',
|
||||
email: 'Enter your email',
|
||||
password: 'Password criteria has not been met',
|
||||
@@ -170,7 +171,8 @@ describe('RegistrationPage', () => {
|
||||
window.location = { href: getConfig().BASE_URL, search: '?next=/course/demo-course-url' };
|
||||
|
||||
const payload = {
|
||||
name: 'John Doe',
|
||||
first_name: 'John',
|
||||
last_name: 'Doe',
|
||||
username: 'john_doe',
|
||||
email: 'john.doe@gmail.com',
|
||||
password: 'password1',
|
||||
@@ -181,9 +183,11 @@ describe('RegistrationPage', () => {
|
||||
};
|
||||
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
populateRequiredFields(registrationPage, payload);
|
||||
registrationPage.find('button.btn-brand').simulate('click');
|
||||
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
populateRequiredFields(getByLabelText, payload);
|
||||
const button = container.querySelector('button.btn-brand');
|
||||
fireEvent.click(button);
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK' }));
|
||||
});
|
||||
|
||||
@@ -191,7 +195,8 @@ describe('RegistrationPage', () => {
|
||||
jest.spyOn(global.Date, 'now').mockImplementation(() => 0);
|
||||
|
||||
const formPayload = {
|
||||
name: 'John Doe',
|
||||
first_name: 'John',
|
||||
last_name: 'Doe',
|
||||
username: 'john_doe',
|
||||
email: 'john.doe@example.com',
|
||||
country: 'Pakistan',
|
||||
@@ -211,10 +216,11 @@ describe('RegistrationPage', () => {
|
||||
},
|
||||
});
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
|
||||
populateRequiredFields(registrationPage, formPayload, true);
|
||||
registrationPage.find('button.btn-brand').simulate('click');
|
||||
populateRequiredFields(getByLabelText, formPayload, true);
|
||||
const button = container.querySelector('button.btn-brand');
|
||||
fireEvent.click(button);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...formPayload, country: 'PK' }));
|
||||
});
|
||||
|
||||
@@ -226,7 +232,8 @@ describe('RegistrationPage', () => {
|
||||
jest.spyOn(global.Date, 'now').mockImplementation(() => 0);
|
||||
|
||||
const payload = {
|
||||
name: 'John Doe',
|
||||
first_name: 'John',
|
||||
last_name: 'Doe',
|
||||
username: 'john_doe',
|
||||
email: 'john.doe@gmail.com',
|
||||
password: 'password1',
|
||||
@@ -237,9 +244,10 @@ describe('RegistrationPage', () => {
|
||||
};
|
||||
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
populateRequiredFields(registrationPage, payload);
|
||||
registrationPage.find('button.btn-brand').simulate('click');
|
||||
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
populateRequiredFields(getByLabelText, payload);
|
||||
const button = container.querySelector('button.btn-brand');
|
||||
fireEvent.click(button);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK' }));
|
||||
|
||||
mergeConfig({
|
||||
@@ -250,25 +258,30 @@ describe('RegistrationPage', () => {
|
||||
it('should not dispatch registerNewUser on empty form Submission', () => {
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
|
||||
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
registrationPage.find('button.btn-brand').simulate('click');
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
|
||||
const button = container.querySelector('button.btn-brand');
|
||||
fireEvent.click(button);
|
||||
|
||||
expect(store.dispatch).not.toHaveBeenCalledWith(registerNewUser({}));
|
||||
});
|
||||
|
||||
// ******** test registration form validations ********
|
||||
|
||||
it('should show error messages for required fields on empty form submission', () => {
|
||||
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
registrationPage.find('button.btn-brand').simulate('click');
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
|
||||
expect(registrationPage.find('div[feedback-for="name"]').text()).toEqual(emptyFieldValidation.name);
|
||||
expect(registrationPage.find('div[feedback-for="username"]').text()).toEqual(emptyFieldValidation.username);
|
||||
expect(registrationPage.find('div[feedback-for="email"]').text()).toEqual(emptyFieldValidation.email);
|
||||
expect(registrationPage.find('div[feedback-for="password"]').text()).toContain(emptyFieldValidation.password);
|
||||
expect(registrationPage.find('div[feedback-for="country"]').text()).toEqual(emptyFieldValidation.country);
|
||||
const button = container.querySelector('button.btn-brand');
|
||||
fireEvent.click(button);
|
||||
|
||||
Object.entries(emptyFieldValidation).forEach(([fieldName, validationMessage]) => {
|
||||
const feedbackElement = container.querySelector(`div[feedback-for="${fieldName}"]`);
|
||||
expect(feedbackElement.textContent).toContain(validationMessage);
|
||||
});
|
||||
|
||||
const alertBanner = 'We couldn\'t create your account.Please check your responses and try again.';
|
||||
expect(registrationPage.find('#validation-errors').first().text()).toEqual(alertBanner);
|
||||
const validationErrors = container.querySelector('#validation-errors');
|
||||
expect(validationErrors.textContent).toContain(alertBanner);
|
||||
});
|
||||
|
||||
it('should set errors with validations returned by registration api', () => {
|
||||
@@ -284,23 +297,28 @@ describe('RegistrationPage', () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />))).find('RegistrationPage');
|
||||
expect(
|
||||
registrationPage.find('div[feedback-for="username"]').text(),
|
||||
).toEqual(usernameError);
|
||||
expect(
|
||||
registrationPage.find('div[feedback-for="email"]').text(),
|
||||
).toEqual(emailError);
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlProvider locale="en"><IntlRegistrationPage {...props} /></IntlProvider>)));
|
||||
const usernameFeedback = container.querySelector('div[feedback-for="username"]');
|
||||
const emailFeedback = container.querySelector('div[feedback-for="email"]');
|
||||
|
||||
expect(usernameFeedback.textContent).toContain(usernameError);
|
||||
expect(emailFeedback.textContent).toContain(emailError);
|
||||
});
|
||||
|
||||
it('should clear error on focus', () => {
|
||||
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
registrationPage.find('button.btn-brand').simulate('click');
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
|
||||
expect(registrationPage.find('div[feedback-for="password"]').text()).toContain(emptyFieldValidation.password);
|
||||
const submitButton = container.querySelector('button.btn-brand');
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
registrationPage.find('input#password').simulate('focus');
|
||||
expect(registrationPage.find('div[feedback-for="password"]').exists()).toBeFalsy();
|
||||
const passwordFeedback = container.querySelector('div[feedback-for="password"]');
|
||||
expect(passwordFeedback.textContent).toContain(emptyFieldValidation.password);
|
||||
|
||||
const passwordField = container.querySelector('input#password');
|
||||
fireEvent.focus(passwordField);
|
||||
|
||||
const isFeedbackPresent = container.contains(passwordFeedback);
|
||||
expect(isFeedbackPresent).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should clear registration backend error on change', () => {
|
||||
@@ -316,19 +334,21 @@ describe('RegistrationPage', () => {
|
||||
});
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
|
||||
const registrationPage = mount(routerWrapper(reduxWrapper(
|
||||
const { container } = render(routerWrapper(reduxWrapper(
|
||||
<IntlRegistrationPage {...props} />,
|
||||
))).find('RegistrationPage');
|
||||
)));
|
||||
|
||||
registrationPage.find('input#email').simulate('change', { target: { value: 'test1@gmail.com', name: 'email' } });
|
||||
const emailInput = container.querySelector('input#email');
|
||||
fireEvent.change(emailInput, { target: { value: 'test1@gmail.com', name: 'email' } });
|
||||
expect(store.dispatch).toHaveBeenCalledWith(clearRegistrationBackendError('email'));
|
||||
});
|
||||
|
||||
// ******** test form buttons and fields ********
|
||||
|
||||
it('should match default button state', () => {
|
||||
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
expect(registrationPage.find('button[type="submit"] span').first().text()).toEqual('Create an account for free');
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
const button = container.querySelector('button[type="submit"] span');
|
||||
expect(button.textContent).toEqual('Create an account for free');
|
||||
});
|
||||
|
||||
it('should match pending button state', () => {
|
||||
@@ -340,10 +360,10 @@ describe('RegistrationPage', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
const button = registrationPage.find('button[type="submit"] span').first();
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
|
||||
expect(button.find('.sr-only').text()).toEqual('pending');
|
||||
const button = container.querySelector('button[type="submit"] span.sr-only');
|
||||
expect(button.textContent).toEqual('pending');
|
||||
});
|
||||
|
||||
it('should display opt-in/opt-out checkbox', () => {
|
||||
@@ -351,8 +371,9 @@ describe('RegistrationPage', () => {
|
||||
MARKETING_EMAILS_OPT_IN: 'true',
|
||||
});
|
||||
|
||||
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
expect(registrationPage.find('div.form-field--checkbox').length).toEqual(1);
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
const checkboxDivs = container.querySelectorAll('div.form-field--checkbox');
|
||||
expect(checkboxDivs.length).toEqual(1);
|
||||
|
||||
mergeConfig({
|
||||
MARKETING_EMAILS_OPT_IN: '',
|
||||
@@ -363,8 +384,12 @@ describe('RegistrationPage', () => {
|
||||
const buttonLabel = 'Register';
|
||||
delete window.location;
|
||||
window.location = { href: getConfig().BASE_URL, search: `?cta=${buttonLabel}` };
|
||||
const registrationPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
|
||||
expect(registrationPage.find('button[type="submit"] span').first().text()).toEqual(buttonLabel);
|
||||
const { container } = render(reduxWrapper(<IntlRegistrationPage {...props} />));
|
||||
const button = container.querySelector('button[type="submit"] span');
|
||||
|
||||
const buttonText = button.textContent;
|
||||
|
||||
expect(buttonText).toEqual(buttonLabel);
|
||||
});
|
||||
|
||||
it('should check user retention cookie', () => {
|
||||
@@ -378,7 +403,7 @@ describe('RegistrationPage', () => {
|
||||
},
|
||||
});
|
||||
|
||||
renderer.create(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
expect(document.cookie).toMatch(`${getConfig().USER_RETENTION_COOKIE_NAME}=true`);
|
||||
});
|
||||
|
||||
@@ -396,7 +421,7 @@ describe('RegistrationPage', () => {
|
||||
});
|
||||
delete window.location;
|
||||
window.location = { href: getConfig().BASE_URL };
|
||||
renderer.create(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
expect(window.location.href).toBe(dashboardURL);
|
||||
});
|
||||
|
||||
@@ -423,7 +448,7 @@ describe('RegistrationPage', () => {
|
||||
});
|
||||
delete window.location;
|
||||
window.location = { href: getConfig().BASE_URL };
|
||||
renderer.create(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
expect(window.location.href).toBe(dashboardUrl);
|
||||
});
|
||||
|
||||
@@ -452,12 +477,11 @@ describe('RegistrationPage', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const progressiveProfilingPage = mount(reduxWrapper(
|
||||
render(reduxWrapper(
|
||||
<Router>
|
||||
<IntlRegistrationPage {...props} />
|
||||
</Router>,
|
||||
));
|
||||
progressiveProfilingPage.update();
|
||||
expect(mockNavigate).toHaveBeenCalledWith(AUTHN_PROGRESSIVE_PROFILING);
|
||||
});
|
||||
|
||||
@@ -473,12 +497,12 @@ describe('RegistrationPage', () => {
|
||||
});
|
||||
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
expect(store.dispatch).toHaveBeenCalledWith(backupRegistrationFormBegin({ ...registrationFormData }));
|
||||
});
|
||||
|
||||
it('should send page event when register page is rendered', () => {
|
||||
mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
expect(sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'register');
|
||||
});
|
||||
|
||||
@@ -496,7 +520,7 @@ describe('RegistrationPage', () => {
|
||||
|
||||
delete window.location;
|
||||
window.location = { href: getConfig().BASE_URL };
|
||||
renderer.create(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.user.account.registered.client', {});
|
||||
});
|
||||
|
||||
@@ -520,10 +544,17 @@ describe('RegistrationPage', () => {
|
||||
},
|
||||
});
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
const { container } = render(reduxWrapper(
|
||||
<Router>
|
||||
<IntlRegistrationPage {...props} />
|
||||
</Router>,
|
||||
));
|
||||
|
||||
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />))).find('RegistrationPage');
|
||||
expect(registrationPage.find('input#email').props().value).toEqual('test@example.com');
|
||||
expect(registrationPage.find('input#username').props().value).toEqual('test');
|
||||
const emailInput = container.querySelector('input#email');
|
||||
const usernameInput = container.querySelector('input#username');
|
||||
|
||||
expect(emailInput.value).toEqual('test@example.com');
|
||||
expect(usernameInput.value).toEqual('test');
|
||||
expect(store.dispatch).toHaveBeenCalledWith(setUserPipelineDataLoaded(true));
|
||||
});
|
||||
|
||||
@@ -538,8 +569,9 @@ describe('RegistrationPage', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />))).find('RegistrationPage');
|
||||
expect(registrationPage.find('div#validation-errors').first().text()).toContain(
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...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.',
|
||||
);
|
||||
});
|
||||
@@ -552,7 +584,8 @@ describe('RegistrationPage', () => {
|
||||
registrationFormData: {
|
||||
...registrationFormData,
|
||||
formFields: {
|
||||
name: 'John Doe',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
username: 'john_doe',
|
||||
email: 'john.doe@yopmail.com',
|
||||
password: 'password1',
|
||||
@@ -564,15 +597,21 @@ describe('RegistrationPage', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const registrationPage = mount(routerWrapper(reduxWrapper(
|
||||
<IntlRegistrationPage {...props} />,
|
||||
))).find('RegistrationPage');
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
|
||||
expect(registrationPage.find('input#name').props().value).toEqual('John Doe');
|
||||
expect(registrationPage.find('input#username').props().value).toEqual('john_doe');
|
||||
expect(registrationPage.find('input#email').props().value).toEqual('john.doe@yopmail.com');
|
||||
expect(registrationPage.find('input#password').props().value).toEqual('password1');
|
||||
expect(registrationPage.find('.email-suggestion-alert-warning').first().text()).toEqual('john.doe@hotmail.com');
|
||||
const firstNameInput = container.querySelector('input#firstName');
|
||||
const lastNameInput = container.querySelector('input#lastName');
|
||||
const usernameInput = container.querySelector('input#username');
|
||||
const emailInput = container.querySelector('input#email');
|
||||
const passwordInput = container.querySelector('input#password');
|
||||
const emailSuggestion = container.querySelector('.email-suggestion-alert-warning');
|
||||
|
||||
expect(firstNameInput.value).toEqual('John');
|
||||
expect(lastNameInput.value).toEqual('Doe');
|
||||
expect(usernameInput.value).toEqual('john_doe');
|
||||
expect(emailInput.value).toEqual('john.doe@yopmail.com');
|
||||
expect(passwordInput.value).toEqual('password1');
|
||||
expect(emailSuggestion.textContent).toEqual('john.doe@hotmail.com');
|
||||
});
|
||||
|
||||
// ********* Embedded experience tests *********/
|
||||
@@ -606,23 +645,22 @@ describe('RegistrationPage', () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
const progressiveProfilingPage = mount(reduxWrapper(
|
||||
<IntlRegistrationPage {...props} />,
|
||||
));
|
||||
progressiveProfilingPage.update();
|
||||
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...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 registrationPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
|
||||
const { container } = render(reduxWrapper(<IntlRegistrationPage {...props} />));
|
||||
|
||||
registrationPage.find('input#username').simulate('blur', { target: { value: '', name: 'username' } });
|
||||
expect(registrationPage.find('div[feedback-for="username"]').exists()).toBeFalsy();
|
||||
const usernameInput = container.querySelector('input#username');
|
||||
fireEvent.blur(usernameInput, { target: { value: '', name: 'username' } });
|
||||
expect(container.querySelector('div[feedback-for="username"]')).toBeFalsy();
|
||||
|
||||
registrationPage.find('input[name="country"]').simulate('blur', { target: { value: '', name: 'country' } });
|
||||
expect(registrationPage.find('div[feedback-for="country"]').exists()).toBeFalsy();
|
||||
const countryInput = container.querySelector('input[name="country"]');
|
||||
fireEvent.blur(countryInput, { target: { value: '', name: 'country' } });
|
||||
expect(container.querySelector('div[feedback-for="country"]')).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should set errors in temporary state when validations are returned by registration api', () => {
|
||||
@@ -641,12 +679,15 @@ describe('RegistrationPage', () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
const registrationPage = mount(routerWrapper(reduxWrapper(
|
||||
const { container } = render(routerWrapper(reduxWrapper(
|
||||
<IntlRegistrationPage {...props} />),
|
||||
)).find('RegistrationPage');
|
||||
));
|
||||
|
||||
expect(registrationPage.find('div[feedback-for="username"]').exists()).toBeFalsy();
|
||||
expect(registrationPage.find('div[feedback-for="email"]').exists()).toBeFalsy();
|
||||
const usernameFeedback = container.querySelector('div[feedback-for="username"]');
|
||||
const emailFeedback = container.querySelector('div[feedback-for="email"]');
|
||||
|
||||
expect(usernameFeedback).toBeNull();
|
||||
expect(emailFeedback).toBeNull();
|
||||
});
|
||||
|
||||
it('should clear error on focus for embedded experience also', () => {
|
||||
@@ -656,13 +697,18 @@ describe('RegistrationPage', () => {
|
||||
search: '?host=http://localhost/host-website',
|
||||
};
|
||||
|
||||
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
registrationPage.find('button.btn-brand').simulate('click');
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
const submitButton = container.querySelector('button.btn-brand');
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
expect(registrationPage.find('div[feedback-for="password"]').text()).toContain(emptyFieldValidation.password);
|
||||
const passwordFeedback = container.querySelector('div[feedback-for="password"]');
|
||||
expect(passwordFeedback.textContent).toContain(emptyFieldValidation.password);
|
||||
|
||||
registrationPage.find('input#password').simulate('focus');
|
||||
expect(registrationPage.find('div[feedback-for="password"]').exists()).toBeFalsy();
|
||||
const passwordField = container.querySelector('input#password');
|
||||
fireEvent.focus(passwordField);
|
||||
|
||||
const updatedPasswordFeedback = container.querySelector('div[feedback-for="password"]');
|
||||
expect(updatedPasswordFeedback).toBeNull();
|
||||
});
|
||||
|
||||
it('should show spinner instead of form while registering if autoSubmitRegForm is true', () => {
|
||||
@@ -682,7 +728,8 @@ describe('RegistrationPage', () => {
|
||||
thirdPartyAuthContext: {
|
||||
...initialState.commonComponents.thirdPartyAuthContext,
|
||||
pipelineUserDetails: {
|
||||
name: 'John Doe',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
username: 'john_doe',
|
||||
email: 'john.doe@example.com',
|
||||
},
|
||||
@@ -692,9 +739,12 @@ describe('RegistrationPage', () => {
|
||||
});
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
|
||||
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
expect(registrationPage.find('#tpa-spinner').exists()).toBeTruthy();
|
||||
expect(registrationPage.find('#registration-form').exists()).toBeFalsy();
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
const spinnerElement = container.querySelector('#tpa-spinner');
|
||||
const registrationFormElement = container.querySelector('#registration-form');
|
||||
|
||||
expect(spinnerElement).toBeTruthy();
|
||||
expect(registrationFormElement).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should auto register if autoSubmitRegForm is true and pipeline details are loaded', () => {
|
||||
@@ -710,7 +760,8 @@ describe('RegistrationPage', () => {
|
||||
registrationFormData: {
|
||||
...registrationFormData,
|
||||
formFields: {
|
||||
name: 'John Doe',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
username: 'john_doe',
|
||||
email: 'john.doe@example.com',
|
||||
},
|
||||
@@ -730,7 +781,8 @@ describe('RegistrationPage', () => {
|
||||
...initialState.commonComponents.thirdPartyAuthContext,
|
||||
currentProvider: 'Apple',
|
||||
pipelineUserDetails: {
|
||||
name: 'John Doe',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
username: 'john_doe',
|
||||
email: 'john.doe@example.com',
|
||||
},
|
||||
@@ -740,9 +792,10 @@ describe('RegistrationPage', () => {
|
||||
});
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
|
||||
mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({
|
||||
name: 'John Doe',
|
||||
first_name: 'John',
|
||||
last_name: 'Doe',
|
||||
username: 'john_doe',
|
||||
email: 'john.doe@example.com',
|
||||
country: 'PK',
|
||||
|
||||
@@ -46,12 +46,6 @@ const ConfigurableRegistrationForm = (props) => {
|
||||
showMarketingEmailOptInCheckbox: getConfig().MARKETING_EMAILS_OPT_IN,
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!formFields.country) {
|
||||
setFormFields(prevState => ({ ...prevState, country: { countryCode: '', displayValue: '' } }));
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* If auto submitting register form, we will check tos and honor code fields if they exist for feature parity.
|
||||
*/
|
||||
|
||||
@@ -2,8 +2,8 @@ import React, { useEffect } from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Alert } from '@edx/paragon';
|
||||
import { Error } from '@edx/paragon/icons';
|
||||
import { Alert } from '@openedx/paragon';
|
||||
import { Error } from '@openedx/paragon/icons';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { windowScrollTo } from '../../data/utils';
|
||||
|
||||
@@ -5,14 +5,14 @@ import { mergeConfig } from '@edx/frontend-platform';
|
||||
import {
|
||||
getLocale, injectIntl, IntlProvider,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { mount } from 'enzyme';
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import configureStore from 'redux-mock-store';
|
||||
|
||||
import ConfigurableRegistrationForm from './ConfigurableRegistrationForm';
|
||||
import { registerNewUser } from '../data/actions';
|
||||
import { FIELDS } from '../data/constants';
|
||||
import RegistrationPage from '../RegistrationPage';
|
||||
import { registerNewUser } from '../../data/actions';
|
||||
import { FIELDS } from '../../data/constants';
|
||||
import RegistrationPage from '../../RegistrationPage';
|
||||
import ConfigurableRegistrationForm from '../ConfigurableRegistrationForm';
|
||||
|
||||
jest.mock('@edx/frontend-platform/analytics', () => ({
|
||||
sendPageEvent: jest.fn(),
|
||||
@@ -56,13 +56,13 @@ describe('ConfigurableRegistrationForm', () => {
|
||||
marketingEmailsOptIn: true,
|
||||
},
|
||||
formFields: {
|
||||
name: '', email: '', username: '', password: '',
|
||||
firstName: '', lastName: '', email: '', username: '', password: '',
|
||||
},
|
||||
emailSuggestion: {
|
||||
suggestion: '', type: '',
|
||||
},
|
||||
errors: {
|
||||
name: '', email: '', username: '', password: '',
|
||||
firstName: '', lastName: '', email: '', username: '', password: '',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -127,16 +127,17 @@ describe('ConfigurableRegistrationForm', () => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const populateRequiredFields = (registrationPage, payload, isThirdPartyAuth = false) => {
|
||||
registrationPage.find('input#name').simulate('change', { target: { value: payload.name, name: 'name' } });
|
||||
registrationPage.find('input#username').simulate('change', { target: { value: payload.username, name: 'username' } });
|
||||
registrationPage.find('input#email').simulate('change', { target: { value: payload.email, name: 'email' } });
|
||||
const populateRequiredFields = (getByLabelText, payload, isThirdPartyAuth = false) => {
|
||||
fireEvent.change(getByLabelText('First name'), { target: { value: payload.first_name, name: 'firstName' } });
|
||||
fireEvent.change(getByLabelText('Last name'), { target: { value: payload.last_name, name: 'lastName' } });
|
||||
fireEvent.change(getByLabelText('Public username'), { target: { value: payload.username, name: 'username' } });
|
||||
fireEvent.change(getByLabelText('Email'), { target: { value: payload.email, name: 'email' } });
|
||||
|
||||
registrationPage.find('input[name="country"]').simulate('change', { target: { value: payload.country, name: 'country' } });
|
||||
registrationPage.find('input[name="country"]').simulate('blur', { target: { value: payload.country, name: 'country' } });
|
||||
fireEvent.change(getByLabelText('Country/Region'), { target: { value: payload.country, name: 'country' } });
|
||||
fireEvent.blur(getByLabelText('Country/Region'), { target: { value: payload.country, name: 'country' } });
|
||||
|
||||
if (!isThirdPartyAuth) {
|
||||
registrationPage.find('input#password').simulate('change', { target: { value: payload.password, name: 'password' } });
|
||||
fireEvent.change(getByLabelText('Password'), { target: { value: payload.password, name: 'password' } });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -157,12 +158,12 @@ describe('ConfigurableRegistrationForm', () => {
|
||||
},
|
||||
};
|
||||
|
||||
const configurableRegistrationForm = mount(routerWrapper(reduxWrapper(
|
||||
render(routerWrapper(reduxWrapper(
|
||||
<IntlConfigurableRegistrationForm {...props} />,
|
||||
)));
|
||||
|
||||
expect(configurableRegistrationForm.find('#profession').exists()).toBeTruthy();
|
||||
expect(configurableRegistrationForm.find('#tos').exists()).toBeTruthy();
|
||||
expect(document.querySelector('#profession')).toBeTruthy();
|
||||
expect(document.querySelector('#tos')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should check TOS and honor code fields if they exist when auto submitting register form', () => {
|
||||
@@ -187,7 +188,7 @@ describe('ConfigurableRegistrationForm', () => {
|
||||
autoSubmitRegistrationForm: true,
|
||||
};
|
||||
|
||||
mount(routerWrapper(reduxWrapper(
|
||||
render(routerWrapper(reduxWrapper(
|
||||
<IntlConfigurableRegistrationForm {...props} />,
|
||||
)));
|
||||
|
||||
@@ -215,9 +216,9 @@ describe('ConfigurableRegistrationForm', () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
expect(registrationPage.find('#profession').exists()).toBeTruthy();
|
||||
expect(registrationPage.find('#tos').exists()).toBeTruthy();
|
||||
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
expect(document.querySelector('#profession')).toBeTruthy();
|
||||
expect(document.querySelector('#tos')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should submit form with fields returned by backend in payload', () => {
|
||||
@@ -238,7 +239,8 @@ describe('ConfigurableRegistrationForm', () => {
|
||||
});
|
||||
|
||||
const payload = {
|
||||
name: 'John Doe',
|
||||
first_name: 'John',
|
||||
last_name: 'Doe',
|
||||
username: 'john_doe',
|
||||
email: 'john.doe@example.com',
|
||||
password: 'password1',
|
||||
@@ -249,11 +251,17 @@ describe('ConfigurableRegistrationForm', () => {
|
||||
};
|
||||
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
|
||||
populateRequiredFields(getByLabelText, payload);
|
||||
|
||||
const professionInput = getByLabelText('Profession');
|
||||
fireEvent.change(professionInput, { target: { value: 'Engineer', name: 'profession' } });
|
||||
|
||||
const submitButton = container.querySelector('button.btn-brand');
|
||||
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
populateRequiredFields(registrationPage, payload);
|
||||
registrationPage.find('input#profession').simulate('change', { target: { value: 'Engineer', name: 'profession' } });
|
||||
registrationPage.find('button.btn-brand').simulate('click');
|
||||
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK' }));
|
||||
});
|
||||
|
||||
@@ -278,12 +286,18 @@ describe('ConfigurableRegistrationForm', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
registrationPage.find('button.btn-brand').simulate('click');
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
const submitButton = container.querySelector('button.btn-brand');
|
||||
|
||||
expect(registrationPage.find('#profession-error').last().text()).toEqual(professionError);
|
||||
expect(registrationPage.find('div[feedback-for="country"]').text()).toEqual(countryError);
|
||||
expect(registrationPage.find('#confirm_email-error').last().text()).toEqual(confirmEmailError);
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
const professionErrorElement = container.querySelector('#profession-error');
|
||||
const countryErrorElement = container.querySelector('div[feedback-for="country"]');
|
||||
const confirmEmailErrorElement = container.querySelector('#confirm_email-error');
|
||||
|
||||
expect(professionErrorElement.textContent).toEqual(professionError);
|
||||
expect(countryErrorElement.textContent).toEqual(countryError);
|
||||
expect(confirmEmailErrorElement.textContent).toEqual(confirmEmailError);
|
||||
});
|
||||
|
||||
it('should show country field validation when country name is invalid', () => {
|
||||
@@ -298,11 +312,17 @@ describe('ConfigurableRegistrationForm', () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
registrationPage.find('input[name="country"]').simulate('blur', { target: { value: 'Pak', name: 'country' } });
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
const countryInput = container.querySelector('input[name="country"]');
|
||||
fireEvent.change(countryInput, { target: { value: 'Pak', name: 'country' } });
|
||||
fireEvent.blur(countryInput, { target: { value: 'Pak', name: 'country' } });
|
||||
|
||||
registrationPage.find('button.btn-brand').simulate('click');
|
||||
expect(registrationPage.find('div[feedback-for="country"]').text()).toEqual(invalidCountryError);
|
||||
const submitButton = container.querySelector('button.btn-brand');
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
const countryErrorElement = container.querySelector('div[feedback-for="country"]');
|
||||
|
||||
expect(countryErrorElement.textContent).toEqual(invalidCountryError);
|
||||
});
|
||||
|
||||
it('should show error if email and confirm email fields do not match', () => {
|
||||
@@ -317,10 +337,17 @@ describe('ConfigurableRegistrationForm', () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
registrationPage.find('input#email').simulate('change', { target: { value: 'test1@gmail.com', name: 'email' } });
|
||||
registrationPage.find('input#confirm_email').simulate('blur', { target: { value: 'test2@gmail.com', name: 'confirm_email' } });
|
||||
expect(registrationPage.find('div#confirm_email-error').text()).toEqual('The email addresses do not match.');
|
||||
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
|
||||
const emailInput = getByLabelText('Email');
|
||||
const confirmEmailInput = getByLabelText('Confirm Email');
|
||||
|
||||
fireEvent.change(emailInput, { target: { value: 'test1@gmail.com', name: 'email' } });
|
||||
fireEvent.blur(confirmEmailInput, { target: { value: 'test2@gmail.com', name: 'confirm_email' } });
|
||||
|
||||
const confirmEmailErrorElement = container.querySelector('div#confirm_email-error');
|
||||
|
||||
expect(confirmEmailErrorElement.textContent).toEqual('The email addresses do not match.');
|
||||
});
|
||||
|
||||
it('should run validations for configurable focused field on form submission', () => {
|
||||
@@ -337,11 +364,19 @@ describe('ConfigurableRegistrationForm', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
registrationPage.find('input#profession').simulate('focus', { target: { value: '', name: 'profession' } });
|
||||
registrationPage.find('button.btn-brand').simulate('click');
|
||||
const { getByLabelText, container } = render(
|
||||
routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)),
|
||||
);
|
||||
|
||||
expect(registrationPage.find('#profession-error').last().text()).toEqual(professionError);
|
||||
const professionInput = getByLabelText('Profession');
|
||||
fireEvent.focus(professionInput);
|
||||
|
||||
const submitButton = container.querySelector('button.btn-brand');
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
const professionErrorElement = container.querySelector('#profession-error');
|
||||
|
||||
expect(professionErrorElement.textContent).toEqual(professionError);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -5,15 +5,15 @@ import { mergeConfig } from '@edx/frontend-platform';
|
||||
import {
|
||||
configure, getLocale, injectIntl, IntlProvider,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { mount } from 'enzyme';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import configureStore from 'redux-mock-store';
|
||||
|
||||
import RegistrationFailureMessage from './RegistrationFailure';
|
||||
import {
|
||||
FORBIDDEN_REQUEST, INTERNAL_SERVER_ERROR, TPA_AUTHENTICATION_FAILURE, TPA_SESSION_EXPIRED,
|
||||
} from '../data/constants';
|
||||
import RegistrationPage from '../RegistrationPage';
|
||||
} from '../../data/constants';
|
||||
import RegistrationPage from '../../RegistrationPage';
|
||||
import RegistrationFailureMessage from '../RegistrationFailure';
|
||||
|
||||
jest.mock('@edx/frontend-platform/analytics', () => ({
|
||||
sendPageEvent: jest.fn(),
|
||||
@@ -137,9 +137,13 @@ describe('RegistrationFailure', () => {
|
||||
failureCount: 0,
|
||||
};
|
||||
|
||||
const registrationPage = mount(reduxWrapper(<IntlRegistrationFailure {...props} />));
|
||||
expect(registrationPage.find('div.alert-heading').length).toEqual(1);
|
||||
expect(registrationPage.find('div.alert').first().text()).toEqual(expectedMessage);
|
||||
const { container } = render(reduxWrapper(<IntlRegistrationFailure {...props} />));
|
||||
|
||||
const alertHeading = container.querySelectorAll('div.alert-heading');
|
||||
expect(alertHeading.length).toEqual(1);
|
||||
|
||||
const alert = container.querySelector('div.alert');
|
||||
expect(alert.textContent).toContain(expectedMessage);
|
||||
});
|
||||
|
||||
it('should match registration api rate limit error message', () => {
|
||||
@@ -149,9 +153,13 @@ describe('RegistrationFailure', () => {
|
||||
failureCount: 0,
|
||||
};
|
||||
|
||||
const registrationPage = mount(reduxWrapper(<IntlRegistrationFailure {...props} />));
|
||||
expect(registrationPage.find('div.alert-heading').length).toEqual(1);
|
||||
expect(registrationPage.find('div.alert').first().text()).toEqual(expectedMessage);
|
||||
const { container } = render(reduxWrapper(<IntlRegistrationFailure {...props} />));
|
||||
|
||||
const alertHeading = container.querySelectorAll('div.alert-heading');
|
||||
expect(alertHeading.length).toEqual(1);
|
||||
|
||||
const alert = container.querySelector('div.alert');
|
||||
expect(alert.textContent).toContain(expectedMessage);
|
||||
});
|
||||
|
||||
it('should match tpa session expired error message', () => {
|
||||
@@ -164,9 +172,13 @@ describe('RegistrationFailure', () => {
|
||||
failureCount: 0,
|
||||
};
|
||||
|
||||
const registrationPage = mount(reduxWrapper(<IntlRegistrationFailure {...props} />));
|
||||
expect(registrationPage.find('div.alert-heading').length).toEqual(1);
|
||||
expect(registrationPage.find('div.alert').first().text()).toEqual(expectedMessage);
|
||||
const { container } = render(reduxWrapper(<IntlRegistrationFailure {...props} />));
|
||||
|
||||
const alertHeading = container.querySelectorAll('div.alert-heading');
|
||||
expect(alertHeading.length).toEqual(1);
|
||||
|
||||
const alert = container.querySelector('div.alert');
|
||||
expect(alert.textContent).toContain(expectedMessage);
|
||||
});
|
||||
|
||||
it('should match tpa authentication failed error message', () => {
|
||||
@@ -179,9 +191,13 @@ describe('RegistrationFailure', () => {
|
||||
failureCount: 0,
|
||||
};
|
||||
|
||||
const registrationPage = mount(reduxWrapper(<IntlRegistrationFailure {...props} />));
|
||||
expect(registrationPage.find('div.alert-heading').length).toEqual(1);
|
||||
expect(registrationPage.find('div.alert').first().text()).toContain(expectedMessageSubstring);
|
||||
const { container } = render(reduxWrapper(<IntlRegistrationFailure {...props} />));
|
||||
|
||||
const alertHeading = container.querySelectorAll('div.alert-heading');
|
||||
expect(alertHeading.length).toEqual(1);
|
||||
|
||||
const alert = container.querySelector('div.alert');
|
||||
expect(alert.textContent).toContain(expectedMessageSubstring);
|
||||
});
|
||||
|
||||
it('should display error message based on the error code returned by API', () => {
|
||||
@@ -195,10 +211,10 @@ describe('RegistrationFailure', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />))).find('RegistrationPage');
|
||||
expect(registrationPage.find('div#validation-errors').first().text()).toContain(
|
||||
'An error has occurred. Try refreshing the page, or check your internet connection.',
|
||||
);
|
||||
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
const validationError = screen.queryByText('An error has occurred. Try refreshing the page, or check your internet connection.');
|
||||
|
||||
expect(validationError).not.toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -5,15 +5,14 @@ import { getConfig, mergeConfig } from '@edx/frontend-platform';
|
||||
import {
|
||||
configure, getLocale, injectIntl, IntlProvider,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { mount } from 'enzyme';
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import renderer from 'react-test-renderer';
|
||||
import configureStore from 'redux-mock-store';
|
||||
|
||||
import {
|
||||
COMPLETE_STATE, LOGIN_PAGE, PENDING_STATE, REGISTER_PAGE,
|
||||
} from '../../data/constants';
|
||||
import RegistrationPage from '../RegistrationPage';
|
||||
} from '../../../data/constants';
|
||||
import RegistrationPage from '../../RegistrationPage';
|
||||
|
||||
jest.mock('@edx/frontend-platform/analytics', () => ({
|
||||
sendPageEvent: jest.fn(),
|
||||
@@ -157,8 +156,13 @@ describe('ThirdPartyAuth', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
expect(registrationPage.find('input#password').length).toEqual(0);
|
||||
const { queryByLabelText } = render(
|
||||
routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />, { store })),
|
||||
);
|
||||
|
||||
const passwordField = queryByLabelText('Password');
|
||||
|
||||
expect(passwordField).toBeNull();
|
||||
});
|
||||
|
||||
it('should render tpa button for tpa_hint id matching one of the primary providers', () => {
|
||||
@@ -177,9 +181,15 @@ describe('ThirdPartyAuth', () => {
|
||||
delete window.location;
|
||||
window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: `?next=/dashboard&tpa_hint=${ssoProvider.id}` };
|
||||
|
||||
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
expect(registrationPage.find(`button#${ssoProvider.id}`).find('span').text()).toEqual(ssoProvider.name);
|
||||
expect(registrationPage.find(`button#${ssoProvider.id}`).hasClass(`btn-tpa btn-${ssoProvider.id}`)).toEqual(true);
|
||||
const { container } = render(
|
||||
routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)),
|
||||
);
|
||||
const tpaButton = container.querySelector(`button#${ssoProvider.id}`);
|
||||
|
||||
expect(tpaButton).toBeTruthy();
|
||||
expect(tpaButton.textContent).toEqual(ssoProvider.name);
|
||||
expect(tpaButton.classList.contains('btn-tpa')).toBe(true);
|
||||
expect(tpaButton.classList.contains(`btn-${ssoProvider.id}`)).toBe(true);
|
||||
});
|
||||
|
||||
it('should display skeleton if tpa_hint is true and thirdPartyAuthContext is pending', () => {
|
||||
@@ -197,8 +207,10 @@ describe('ThirdPartyAuth', () => {
|
||||
search: `?next=/dashboard&tpa_hint=${ssoProvider.id}`,
|
||||
};
|
||||
|
||||
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
expect(registrationPage.find('.react-loading-skeleton').exists()).toBeTruthy();
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
const skeletonElement = container.querySelector('.react-loading-skeleton');
|
||||
|
||||
expect(skeletonElement).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render icon if icon classes are missing in providers', () => {
|
||||
@@ -219,8 +231,10 @@ describe('ThirdPartyAuth', () => {
|
||||
window.location = { href: getConfig().BASE_URL.concat(REGISTER_PAGE), search: `?next=/dashboard&tpa_hint=${ssoProvider.id}` };
|
||||
ssoProvider.iconImage = null;
|
||||
|
||||
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
expect(registrationPage.find(`button#${ssoProvider.id}`).find('div').find('span').hasClass('pgn__icon')).toEqual(true);
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
const iconElement = container.querySelector(`button#${ssoProvider.id} div span.pgn__icon`);
|
||||
|
||||
expect(iconElement).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render tpa button for tpa_hint id matching one of the secondary providers', () => {
|
||||
@@ -240,7 +254,7 @@ describe('ThirdPartyAuth', () => {
|
||||
delete window.location;
|
||||
window.location = { href: getConfig().BASE_URL.concat(REGISTER_PAGE), search: `?next=/dashboard&tpa_hint=${secondaryProviders.id}` };
|
||||
|
||||
mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
expect(window.location.href).toEqual(getConfig().LMS_BASE_URL + secondaryProviders.registerUrl);
|
||||
});
|
||||
|
||||
@@ -261,8 +275,10 @@ describe('ThirdPartyAuth', () => {
|
||||
delete window.location;
|
||||
window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: '?next=/dashboard&tpa_hint=invalid' };
|
||||
|
||||
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
expect(registrationPage.find(`button#${ssoProvider.id}`).find('span#provider-name').text()).toEqual(expectedMessage);
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
const providerButton = container.querySelector(`button#${ssoProvider.id} span#provider-name`);
|
||||
|
||||
expect(providerButton.textContent).toEqual(expectedMessage);
|
||||
});
|
||||
|
||||
it('should show single sign on provider button', () => {
|
||||
@@ -277,8 +293,13 @@ describe('ThirdPartyAuth', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
expect(registrationPage.find(`button#${ssoProvider.id}`).length).toEqual(1);
|
||||
const { container } = render(
|
||||
routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />, { store })),
|
||||
);
|
||||
|
||||
const buttonsWithId = container.querySelectorAll(`button#${ssoProvider.id}`);
|
||||
|
||||
expect(buttonsWithId.length).toEqual(1);
|
||||
});
|
||||
|
||||
it('should show single sign on provider button', () => {
|
||||
@@ -293,8 +314,13 @@ describe('ThirdPartyAuth', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
expect(registrationPage.find(`button#${ssoProvider.id}`).length).toEqual(1);
|
||||
const { container } = render(
|
||||
routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)),
|
||||
);
|
||||
|
||||
const buttonsWithId = container.querySelectorAll(`button#${ssoProvider.id}`);
|
||||
|
||||
expect(buttonsWithId.length).toEqual(1);
|
||||
});
|
||||
|
||||
it('should display InstitutionLogistration if insitutionLogin prop is true', () => {
|
||||
@@ -303,8 +329,9 @@ describe('ThirdPartyAuth', () => {
|
||||
institutionLogin: true,
|
||||
};
|
||||
|
||||
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
expect(registrationPage.find('.institutions__heading').text()).toEqual('Register with institution/campus credentials');
|
||||
const { getByText } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
const headingElement = getByText('Register with institution/campus credentials');
|
||||
expect(headingElement).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should redirect to social auth provider url on SSO button click', () => {
|
||||
@@ -326,9 +353,13 @@ describe('ThirdPartyAuth', () => {
|
||||
delete window.location;
|
||||
window.location = { href: getConfig().BASE_URL };
|
||||
|
||||
const loginPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
const { container } = render(
|
||||
routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)),
|
||||
);
|
||||
|
||||
const ssoButton = container.querySelector('button#oa2-apple-id');
|
||||
fireEvent.click(ssoButton);
|
||||
|
||||
loginPage.find('button#oa2-apple-id').simulate('click');
|
||||
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + registerUrl);
|
||||
});
|
||||
|
||||
@@ -354,7 +385,7 @@ describe('ThirdPartyAuth', () => {
|
||||
delete window.location;
|
||||
window.location = { href: getConfig().BASE_URL };
|
||||
|
||||
renderer.create(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + authCompleteUrl);
|
||||
});
|
||||
|
||||
@@ -375,9 +406,11 @@ 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 registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
expect(registrationPage.find('#tpa-alert').find('p').text()).toEqual(expectedMessage);
|
||||
const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
const tpaAlert = container.querySelector('#tpa-alert p');
|
||||
expect(tpaAlert.textContent).toEqual(expectedMessage);
|
||||
});
|
||||
|
||||
it('should display errorMessage if third party authentication fails', () => {
|
||||
jest.spyOn(global.Date, 'now').mockImplementation(() => 0);
|
||||
getLocale.mockImplementation(() => ('en-us'));
|
||||
@@ -403,9 +436,15 @@ describe('ThirdPartyAuth', () => {
|
||||
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
|
||||
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
expect(registrationPage.find('div.alert-heading').length).toEqual(1);
|
||||
expect(registrationPage.find('div.alert').first().text()).toContain('An error occurred');
|
||||
const { container } = render(
|
||||
routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)),
|
||||
);
|
||||
|
||||
const alertHeading = container.querySelector('div.alert-heading');
|
||||
expect(alertHeading).toBeTruthy();
|
||||
|
||||
const alert = container.querySelector('div.alert');
|
||||
expect(alert.textContent).toContain('An error occurred');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -22,13 +22,13 @@ export const defaultState = {
|
||||
marketingEmailsOptIn: true,
|
||||
},
|
||||
formFields: {
|
||||
name: '', email: '', username: '', password: '',
|
||||
firstName: '', lastName: '', email: '', username: '', password: '',
|
||||
},
|
||||
emailSuggestion: {
|
||||
suggestion: '', type: '',
|
||||
},
|
||||
errors: {
|
||||
name: '', email: '', username: '', password: '',
|
||||
firstName: '', lastName: '', email: '', username: '', password: '',
|
||||
},
|
||||
},
|
||||
validations: null,
|
||||
|
||||
@@ -22,13 +22,13 @@ describe('Registration Reducer Tests', () => {
|
||||
marketingEmailsOptIn: true,
|
||||
},
|
||||
formFields: {
|
||||
name: '', email: '', username: '', password: '',
|
||||
firstName: '', lastName: '', email: '', username: '', password: '',
|
||||
},
|
||||
emailSuggestion: {
|
||||
suggestion: '', type: '',
|
||||
},
|
||||
errors: {
|
||||
name: '', email: '', username: '', password: '',
|
||||
firstName: '', lastName: '', email: '', username: '', password: '',
|
||||
},
|
||||
},
|
||||
validations: null,
|
||||
@@ -64,6 +64,7 @@ describe('Registration Reducer Tests', () => {
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should set redirect url dashboard on registration success action', () => {
|
||||
const payload = {
|
||||
redirectUrl: `${getConfig().BASE_URL}${DEFAULT_REDIRECT_URL}`,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getConfig, snakeCaseObject } from '@edx/frontend-platform';
|
||||
import { snakeCaseObject } from '@edx/frontend-platform';
|
||||
|
||||
import { LETTER_REGEX, NUMBER_REGEX } from '../../data/constants';
|
||||
import messages from '../messages';
|
||||
@@ -44,15 +44,15 @@ export const isFormValid = (
|
||||
}
|
||||
});
|
||||
|
||||
if (getConfig().SHOW_CONFIGURABLE_EDX_FIELDS) {
|
||||
if (!configurableFormFields?.country?.displayValue) {
|
||||
fieldErrors.country = formatMessage(messages['empty.country.field.error']);
|
||||
isValid = false;
|
||||
} else if (!configurableFormFields?.country?.countryCode) {
|
||||
fieldErrors.country = formatMessage(messages['invalid.country.field.error']);
|
||||
isValid = false;
|
||||
}
|
||||
// Don't validate when country field is optional or hidden and not present on registration form
|
||||
if (configurableFormFields?.country && !configurableFormFields.country?.displayValue) {
|
||||
fieldErrors.country = formatMessage(messages['empty.country.field.error']);
|
||||
isValid = false;
|
||||
} else if (configurableFormFields?.country && !configurableFormFields.country?.countryCode) {
|
||||
fieldErrors.country = formatMessage(messages['invalid.country.field.error']);
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
Object.keys(fieldDescriptions).forEach(key => {
|
||||
if (key === 'country' && !configurableFormFields.country.displayValue) {
|
||||
fieldErrors[key] = formatMessage(messages['empty.country.field.error']);
|
||||
|
||||
@@ -7,10 +7,15 @@ const messages = defineMessages({
|
||||
description: 'register page title',
|
||||
},
|
||||
// Field labels
|
||||
'registration.fullname.label': {
|
||||
id: 'registration.fullname.label',
|
||||
defaultMessage: 'Full name',
|
||||
description: 'Label that appears above fullname field',
|
||||
'registration.firstName.label': {
|
||||
id: 'registration.firstName.label',
|
||||
defaultMessage: 'First name',
|
||||
description: 'Label that appears above first name field',
|
||||
},
|
||||
'registration.lastName.label': {
|
||||
id: 'registration.lastName.label',
|
||||
defaultMessage: 'Last name',
|
||||
description: 'Label that appears above last name field',
|
||||
},
|
||||
'registration.email.label': {
|
||||
id: 'registration.email.label',
|
||||
@@ -38,10 +43,10 @@ const messages = defineMessages({
|
||||
description: 'Text for opt in option on register page.',
|
||||
},
|
||||
// Help text
|
||||
'help.text.name': {
|
||||
id: 'help.text.name',
|
||||
'help.text.firstName': {
|
||||
id: 'help.text.firstName',
|
||||
defaultMessage: 'This name will be used by any certificates that you earn.',
|
||||
description: 'Help text for fullname field on registration page',
|
||||
description: 'Help text for first name field on registration page',
|
||||
},
|
||||
'help.text.username.1': {
|
||||
id: 'help.text.username.1',
|
||||
@@ -64,32 +69,27 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Create an account for free',
|
||||
description: 'Label text for registration form submission button',
|
||||
},
|
||||
'registration.other.options.heading': {
|
||||
id: 'registration.other.options.heading',
|
||||
defaultMessage: 'Or register with:',
|
||||
description: 'A message that appears above third party auth providers i.e saml, google, facebook etc',
|
||||
},
|
||||
'create.account.cta.button': {
|
||||
id: 'create.account.cta.button',
|
||||
defaultMessage: '{label}',
|
||||
description: 'Label text for registration form submission button for those users who are landing through redirections',
|
||||
},
|
||||
// Institution login
|
||||
'register.institution.login.button': {
|
||||
id: 'register.institution.login.button',
|
||||
defaultMessage: 'Institution/campus credentials',
|
||||
description: 'shows institutions list',
|
||||
},
|
||||
'register.institution.login.page.title': {
|
||||
id: 'register.institution.login.page.title',
|
||||
defaultMessage: 'Register with institution/campus credentials',
|
||||
description: 'Heading of institution page',
|
||||
},
|
||||
// Validation messages
|
||||
'empty.name.field.error': {
|
||||
id: 'empty.name.field.error',
|
||||
defaultMessage: 'Enter your full name',
|
||||
description: 'Error message for empty fullname field',
|
||||
'empty.firstName.field.error': {
|
||||
id: 'empty.firstName.field.error',
|
||||
defaultMessage: 'Enter your first name',
|
||||
description: 'Error message for empty first name field',
|
||||
},
|
||||
'empty.lastName.field.error': {
|
||||
id: 'empty.lastName.field.error',
|
||||
defaultMessage: 'Enter your last name',
|
||||
description: 'Error message for empty last name field',
|
||||
},
|
||||
'empty.email.field.error': {
|
||||
id: 'empty.email.field.error',
|
||||
@@ -131,10 +131,15 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Username must be between 2 and 30 characters',
|
||||
description: 'Error message for empty username field',
|
||||
},
|
||||
'name.validation.message': {
|
||||
id: 'name.validation.message',
|
||||
defaultMessage: 'Enter a valid name',
|
||||
description: 'Validation message that appears when fullname contain URL',
|
||||
'firstName.validation.message': {
|
||||
id: 'firstName.validation.message',
|
||||
defaultMessage: 'Enter a valid first name',
|
||||
description: 'Validation message that appears when first name contain URL',
|
||||
},
|
||||
'lastName.validation.message': {
|
||||
id: 'lastName.validation.message',
|
||||
defaultMessage: 'Enter a valid last name',
|
||||
description: 'Validation message that appears when last name contain URL',
|
||||
},
|
||||
'password.validation.message': {
|
||||
id: 'password.validation.message',
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Alert } from '@edx/paragon';
|
||||
import { Error } from '@edx/paragon/icons';
|
||||
import { Alert } from '@openedx/paragon';
|
||||
import { Error } from '@openedx/paragon/icons';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { FORM_SUBMISSION_ERROR, PASSWORD_RESET, PASSWORD_VALIDATION_ERROR } from './data/constants';
|
||||
|
||||
@@ -10,8 +10,8 @@ import {
|
||||
StatefulButton,
|
||||
Tab,
|
||||
Tabs,
|
||||
} from '@edx/paragon';
|
||||
import { ChevronLeft } from '@edx/paragon/icons';
|
||||
} from '@openedx/paragon';
|
||||
import { ChevronLeft } from '@openedx/paragon/icons';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Alert } from '@edx/paragon';
|
||||
import { Alert } from '@openedx/paragon';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
@@ -9,7 +9,7 @@ const ResetPasswordSuccess = () => {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
return (
|
||||
<Alert id="reset-password-success" variant="success" className="mb-4">
|
||||
<Alert id="reset-password-success" variant="success" className="mb-5">
|
||||
<Alert.Heading>
|
||||
{formatMessage(messages['reset.password.success.heading'])}
|
||||
</Alert.Heading>
|
||||
|
||||
@@ -109,7 +109,7 @@
|
||||
}
|
||||
|
||||
.form-field-error {
|
||||
border: 2px solid var(--danger-300, #CA3A2F) !important;
|
||||
border: 1px solid var(--danger-300, #CA3A2F) !important;
|
||||
|
||||
input {
|
||||
border: none;
|
||||
|
||||
@@ -3,10 +3,7 @@
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { configure as configureLogging } from '@edx/frontend-platform/logging';
|
||||
import Adapter from '@wojtekmaj/enzyme-adapter-react-17';
|
||||
import Enzyme from 'enzyme';
|
||||
|
||||
Enzyme.configure({ adapter: new Adapter() });
|
||||
class MockLoggingService {
|
||||
logInfo = jest.fn();
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const { createConfig } = require('@edx/frontend-build');
|
||||
const { createConfig } = require('@openedx/frontend-build');
|
||||
|
||||
const config = createConfig('webpack-prod');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user