Compare commits
88 Commits
embeddable
...
test-sandb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e29a461a34 | ||
|
|
9276f25200 | ||
|
|
dc90cf9ce5 | ||
|
|
36354761cc | ||
|
|
f53add81f3 | ||
|
|
bca59ebd40 | ||
|
|
dcb5da42b0 | ||
|
|
b346c22b57 | ||
|
|
8be350e35f | ||
|
|
6695fb6f61 | ||
|
|
be5b0bb461 | ||
|
|
5f93278326 | ||
|
|
488644f50d | ||
|
|
56bab26018 | ||
|
|
ca42f3851d | ||
|
|
e4bddc2db0 | ||
|
|
8aeacaa001 | ||
|
|
80435d3e5b | ||
|
|
d6c5415c9a | ||
|
|
0306763eeb | ||
|
|
e4ac1288a9 | ||
|
|
e1f489838c | ||
|
|
9b046146a0 | ||
|
|
e0d605582e | ||
|
|
21b5a01cab | ||
|
|
955ea6485f | ||
|
|
9f8a1af7e3 | ||
|
|
e617a3ba40 | ||
|
|
fb3f962039 | ||
|
|
64da54f17c | ||
|
|
74741a1be6 | ||
|
|
e9aaf7024a | ||
|
|
e3d96385ee | ||
|
|
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 | ||
|
|
10a10c8ed9 | ||
|
|
800a5fc6be | ||
|
|
924488c29b | ||
|
|
dae050ecb3 | ||
|
|
3a31cf33e2 | ||
|
|
66a0d5d840 | ||
|
|
26caf857bf | ||
|
|
fa34eae800 | ||
|
|
644b16580d | ||
|
|
479dac8397 | ||
|
|
c097b5b831 | ||
|
|
78722f3e73 | ||
|
|
7f8a270770 | ||
|
|
4ff14c8731 | ||
|
|
a957973105 | ||
|
|
f0b855d87e | ||
|
|
446649735d | ||
|
|
397c237e30 | ||
|
|
e10d6b6384 | ||
|
|
38c186d5a7 | ||
|
|
55c320a88a | ||
|
|
6ec0a22194 | ||
|
|
7bae030713 | ||
|
|
7b4714a22a | ||
|
|
332d6abee7 | ||
|
|
4df13cf0b7 |
4
.env
4
.env
@@ -15,7 +15,7 @@ SEGMENT_KEY=''
|
||||
SITE_NAME=null
|
||||
INFO_EMAIL=''
|
||||
# ***** Cookies *****
|
||||
REGISTER_CONVERSION_COOKIE_NAME=null
|
||||
USER_RETENTION_COOKIE_NAME=null
|
||||
# ***** Links *****
|
||||
LOGIN_ISSUE_SUPPORT_LINK=''
|
||||
AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK=null
|
||||
@@ -23,11 +23,13 @@ POST_REGISTRATION_REDIRECT_URL=''
|
||||
SEARCH_CATALOG_URL=''
|
||||
# ***** Features flags *****
|
||||
DISABLE_ENTERPRISE_LOGIN=''
|
||||
ENABLE_AUTO_GENERATED_USERNAME=''
|
||||
ENABLE_DYNAMIC_REGISTRATION_FIELDS=''
|
||||
ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN=''
|
||||
ENABLE_POST_REGISTRATION_RECOMMENDATIONS=''
|
||||
MARKETING_EMAILS_OPT_IN=''
|
||||
SHOW_CONFIGURABLE_EDX_FIELDS=''
|
||||
ENABLE_IMAGE_LAYOUT=''
|
||||
# ***** Zendesk related keys *****
|
||||
ZENDESK_KEY=''
|
||||
ZENDESK_LOGO_URL=''
|
||||
|
||||
@@ -19,8 +19,10 @@ 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 *****
|
||||
REGISTER_CONVERSION_COOKIE_NAME='openedx-user-register-conversion'
|
||||
SESSION_COOKIE_DOMAIN='localhost'
|
||||
USER_INFO_COOKIE_NAME='edx-user-info'
|
||||
# ***** Links *****
|
||||
|
||||
@@ -16,6 +16,5 @@ ORDER_HISTORY_URL='http://localhost:1996/orders'
|
||||
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
|
||||
SEGMENT_KEY=''
|
||||
SITE_NAME='Your Platform Name Here'
|
||||
REGISTER_CONVERSION_COOKIE_NAME='openedx-user-register-conversion'
|
||||
APP_ID=''
|
||||
MFE_CONFIG_API_URL=''
|
||||
|
||||
@@ -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',
|
||||
|
||||
2
.github/pull_request_template.md
vendored
2
.github/pull_request_template.md
vendored
@@ -25,5 +25,5 @@ Include a link to the sandbox for design changes or screenshot for before and af
|
||||
|
||||
#### Post-merge Checklist
|
||||
|
||||
* [ ] Deploy the changes to prod after verifying on stage or ask **@openedx/vanguards** to do it.
|
||||
* [ ] Deploy the changes to prod after verifying on stage or ask **@openedx/2u-vanguards** to do it.
|
||||
* [ ] 🎉 🙌 Celebrate! Thanks for your contribution.
|
||||
|
||||
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -41,4 +41,4 @@ jobs:
|
||||
run: npm run build
|
||||
|
||||
- name: Run Code Coverage
|
||||
uses: codecov/codecov-action@v3
|
||||
uses: codecov/codecov-action@v4
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
# The following users are the owners of all frontend-app-authn files
|
||||
* @openedx/vanguards
|
||||
* @openedx/2u-vanguards
|
||||
|
||||
9
Makefile
9
Makefile
@@ -7,14 +7,14 @@ i18n = ./src/i18n
|
||||
transifex_input = $(i18n)/transifex_input.json
|
||||
|
||||
# This directory must match .babelrc .
|
||||
transifex_temp = ./temp/babel-plugin-react-intl
|
||||
transifex_temp = ./temp/babel-plugin-formatjs
|
||||
|
||||
precommit:
|
||||
npm run lint
|
||||
npm audit
|
||||
|
||||
requirements:
|
||||
npm install
|
||||
npm ci
|
||||
|
||||
i18n.extract:
|
||||
# Pulling display strings from .jsx files into .json files...
|
||||
@@ -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.
|
||||
|
||||
70
README.rst
70
README.rst
@@ -1,12 +1,12 @@
|
||||
##################
|
||||
frontend-app-authn
|
||||
##################
|
||||
|
||||
|Build Status| |ci-badge| |Codecov| |semantic-release|
|
||||
|
||||
frontend-app-authn
|
||||
====================
|
||||
|
||||
Please tag **@openedx/vanguards** on any PRs or issues. Thanks!
|
||||
|
||||
Introduction
|
||||
------------
|
||||
********
|
||||
Purpose
|
||||
********
|
||||
|
||||
This is a micro-frontend application responsible for the login, registration and password reset functionality.
|
||||
|
||||
@@ -22,9 +22,12 @@ This is a micro-frontend application responsible for the login, registration and
|
||||
|
||||
- Progressive profiling page
|
||||
|
||||
***************
|
||||
Getting Started
|
||||
***************
|
||||
|
||||
Installation
|
||||
------------
|
||||
============
|
||||
|
||||
This MFE is bundled with `Devstack <https://github.com/openedx/devstack>`_, see the `Getting Started <https://github.com/openedx/devstack#getting-started>`_ section for setup instructions.
|
||||
|
||||
@@ -46,7 +49,7 @@ This MFE is bundled with `Devstack <https://github.com/openedx/devstack>`_, see
|
||||
**Note:** Follow `Enable social auth locally <docs/how_tos/enable_social_auth.rst>`_ for enabling Social Sign-on Buttons (SSO) locally
|
||||
|
||||
Environment Variables/Setup Notes
|
||||
---------------------------------
|
||||
=================================
|
||||
|
||||
This MFE is configured via environment variables supplied at build time. All micro-frontends have a shared set of required environment variables, as documented in the Open edX Developer Guide under `Required Environment Variables <https://edx.readthedocs.io/projects/edx-developer-docs/en/latest/developers_guide/micro_frontends_in_open_edx.html#required-environment-variables>`__.
|
||||
|
||||
@@ -112,9 +115,13 @@ 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
|
||||
**********************************
|
||||
==================================
|
||||
|
||||
Furthermore, there are several edX-specific environment variables that enable integrations with closed-source services private to the edX organization, and might be unsupported in Open edX.
|
||||
|
||||
@@ -138,7 +145,8 @@ For more information see the document: `Micro-frontend applications in Open
|
||||
edX <https://edx.readthedocs.io/projects/edx-developer-docs/en/latest/developers_guide/micro_frontends_in_open_edx.html#required-environment-variables>`__.
|
||||
|
||||
How To Contribute
|
||||
------------
|
||||
=================
|
||||
|
||||
Contributions are very welcome, and strongly encouraged! We've
|
||||
put together `some documentation that describes our contribution process <https://edx.readthedocs.org/projects/edx-developer-guide/en/latest/process/index.html>`_.
|
||||
|
||||
@@ -149,34 +157,58 @@ can find it it at `PULL_REQUEST_TEMPLATE.md <https://github.com/openedx/frontend
|
||||
|
||||
This project is currently accepting all types of contributions, bug fixes and security fixes.
|
||||
|
||||
Open edX Code of Conduct
|
||||
------------------------
|
||||
Getting Help
|
||||
============
|
||||
|
||||
If you're having trouble, we have discussion forums at
|
||||
https://discuss.openedx.org where you can connect with others in the community.
|
||||
|
||||
Our real-time conversations are on Slack. You can request a `Slack
|
||||
invitation`_, then join our `community Slack workspace`_. Because this is a
|
||||
frontend repository, the best place to discuss it would be in the `#wg-frontend
|
||||
channel`_.
|
||||
|
||||
For anything non-trivial, the best path is to open an issue in this repository
|
||||
with as many details about the issue you are facing as you can provide.
|
||||
|
||||
https://github.com/openedx/frontend-app-authn/issues
|
||||
|
||||
For more information about these options, see the `Getting Help`_ page.
|
||||
|
||||
.. _Slack invitation: https://openedx.org/slack
|
||||
.. _community Slack workspace: https://openedx.slack.com/
|
||||
.. _#wg-frontend channel: https://openedx.slack.com/archives/C04BM6YC7A6
|
||||
.. _Getting Help: https://openedx.org/community/connect
|
||||
|
||||
The Open edX Code of Conduct
|
||||
============================
|
||||
All community members are expected to follow the `Open edX Code of Conduct <https://openedx.org/code-of-conduct/>`_.
|
||||
|
||||
People
|
||||
------
|
||||
======
|
||||
The assigned maintainers for this component and other project details may be
|
||||
found in `Backstage <https://backstage.openedx.org/catalog/default/group/vanguards>`_. Backstage pulls this data from the ``catalog-info.yaml``
|
||||
found in `Backstage <https://backstage.openedx.org/catalog/default/group/2u-vanguards>`_. Backstage pulls this data from the ``catalog-info.yaml``
|
||||
file in this repo.
|
||||
|
||||
Reporting Security Issues
|
||||
-------------------------
|
||||
=========================
|
||||
|
||||
Please do not report security issues in public. Please email security@edx.org.
|
||||
Please do not report security issues in public. Please email security@openedx.org.
|
||||
|
||||
Known Issues
|
||||
------------
|
||||
============
|
||||
|
||||
None
|
||||
|
||||
License
|
||||
-------
|
||||
=======
|
||||
|
||||
The code in this repository is licensed under the GNU Affero General Public License v3.0, unless
|
||||
otherwise noted.
|
||||
|
||||
Please see `LICENSE <https://github.com/openedx/frontend-app-authn/blob/master/LICENSE>`_ for details.
|
||||
|
||||
|
||||
==============================
|
||||
|
||||
.. |Build Status| image:: https://api.travis-ci.com/edx/frontend-app-authn.svg?branch=master
|
||||
|
||||
@@ -13,6 +13,6 @@ metadata:
|
||||
annotations:
|
||||
openedx.org/arch-interest-groups: ""
|
||||
spec:
|
||||
owner: group:vanguards
|
||||
owner: group:2u-vanguards
|
||||
type: 'service'
|
||||
lifecycle: 'production'
|
||||
|
||||
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: [
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
|
||||
nick: Authn MFE
|
||||
oeps: {}
|
||||
owner: openedx/vanguards
|
||||
owner: openedx/2u-vanguards
|
||||
openedx-release:
|
||||
ref: master
|
||||
|
||||
13185
package-lock.json
generated
13185
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
43
package.json
43
package.json
@@ -11,7 +11,7 @@
|
||||
],
|
||||
"scripts": {
|
||||
"build": "fedx-scripts webpack",
|
||||
"i18n_extract": "BABEL_ENV=i18n fedx-scripts babel src --quiet > /dev/null",
|
||||
"i18n_extract": "fedx-scripts formatjs extract",
|
||||
"lint": "fedx-scripts eslint --ext .js --ext .jsx .",
|
||||
"snapshot": "fedx-scripts jest --updateSnapshot",
|
||||
"start": "fedx-scripts webpack-dev-server --progress",
|
||||
@@ -32,55 +32,54 @@
|
||||
"url": "https://github.com/openedx/frontend-app-authn/issues"
|
||||
},
|
||||
"dependencies": {
|
||||
"@edx/brand": "npm:@edx/brand-openedx@1.2.0",
|
||||
"@edx/frontend-platform": "^5.0.0",
|
||||
"@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/brand": "npm:@openedx/brand-openedx@^1.2.2",
|
||||
"@edx/frontend-platform": "7.1.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.36.0",
|
||||
"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",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-helmet": "6.1.0",
|
||||
"react-loading-skeleton": "3.3.1",
|
||||
"react-loading-skeleton": "3.4.0",
|
||||
"react-redux": "7.2.9",
|
||||
"react-responsive": "8.2.0",
|
||||
"react-router": "6.15.0",
|
||||
"react-router-dom": "6.15.0",
|
||||
"react-router": "6.22.3",
|
||||
"react-router-dom": "6.22.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": "12.9.8",
|
||||
"@edx/reactifex": "1.1.0",
|
||||
"@wojtekmaj/enzyme-adapter-react-17": "^0.8.0",
|
||||
"babel-plugin-formatjs": "10.5.3",
|
||||
"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,11 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="en-us">
|
||||
<head>
|
||||
<title>Authn | <%= process.env.SITE_NAME %></title>
|
||||
<title><%= (process.env.SITE_NAME && process.env.SITE_NAME != 'null') ? 'Authentication | ' + process.env.SITE_NAME : 'Authentication' %></title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/iframe-resizer/4.3.6/iframeResizer.contentWindow.min.js"
|
||||
integrity="sha512-R7Piufj0/o6jG9ZKrAvS2dblFr2kkuG4XVQwStX+/4P+KwOLUXn2DXy0l1AJDxxqGhkM/FJllZHG2PKOAheYzg=="
|
||||
<link rel="shortcut icon" href="<%=htmlWebpackPlugin.options.FAVICON_URL%>" type="image/x-icon"/>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/iframe-resizer/4.3.9/iframeResizer.contentWindow.min.js"
|
||||
integrity="sha512-mdT/HQRzoRP4laVz49Mndx6rcCGA3IhuyhP3gaY0E9sZPkwbtDk9ttQIq9o8qGCf5VvJv1Xsy3k2yTjfUoczqw=="
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer">
|
||||
</script>
|
||||
|
||||
@@ -24,7 +24,7 @@ import { ForgotPasswordPage } from './forgot-password';
|
||||
import Logistration from './logistration/Logistration';
|
||||
import { ProgressiveProfiling } from './progressive-profiling';
|
||||
import { RecommendationsPage } from './recommendations';
|
||||
import { EmbeddableRegistrationPage } from './register';
|
||||
import { RegistrationPage } from './register';
|
||||
import { ResetPasswordPage } from './reset-password';
|
||||
|
||||
import './index.scss';
|
||||
@@ -41,7 +41,7 @@ const MainApp = () => (
|
||||
<Route path="/" element={<Navigate replace to={updatePathWithQueryParams(REGISTER_PAGE)} />} />
|
||||
<Route
|
||||
path={REGISTER_EMBEDDED_PAGE}
|
||||
element={<EmbeddedRegistrationRoute><EmbeddableRegistrationPage /></EmbeddedRegistrationRoute>}
|
||||
element={<EmbeddedRegistrationRoute><RegistrationPage /></EmbeddedRegistrationRoute>}
|
||||
/>
|
||||
<Route
|
||||
path={LOGIN_PAGE}
|
||||
|
||||
@@ -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';
|
||||
@@ -23,13 +23,15 @@ const MediumLayout = () => {
|
||||
<div>
|
||||
<h1
|
||||
className={classNames(
|
||||
'display-1 text-white mt-5 mb-5 mr-2',
|
||||
'display-1 text-white mt-5 mb-5 mr-2 main-heading',
|
||||
{ 'ml-4.5': getConfig().SITE_NAME !== 'edX' },
|
||||
)}
|
||||
>
|
||||
<span className="mr-2">{formatMessage(messages['start.learning'])}</span>
|
||||
<span className="text-accent-a d-inline-block">
|
||||
{formatMessage(messages['with.site.name'], { siteName: getConfig().SITE_NAME })}
|
||||
<span>
|
||||
{formatMessage(messages['start.learning'])}{' '}
|
||||
<span className="text-accent-a d-inline-block">
|
||||
{formatMessage(messages['with.site.name'], { siteName: getConfig().SITE_NAME })}
|
||||
</span>
|
||||
</span>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
@@ -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';
|
||||
@@ -17,17 +17,18 @@ const SmallLayout = () => {
|
||||
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}>
|
||||
<Image className="logo-small" alt={getConfig().SITE_NAME} src={getConfig().LOGO_WHITE_URL} />
|
||||
</Hyperlink>
|
||||
<div className="d-flex align-items-center mb-3 mt-3 mr-3">
|
||||
<div className="d-flex align-items-center m-3.5">
|
||||
<div className={classNames({ 'small-yellow-line mr-n2.5': getConfig().SITE_NAME === 'edX' })} />
|
||||
<h1
|
||||
className={classNames(
|
||||
'text-white mt-3.5 mb-3.5',
|
||||
{ 'ml-4.5': getConfig().SITE_NAME !== 'edX' },
|
||||
)}
|
||||
>
|
||||
<span className="mr-1">{formatMessage(messages['start.learning'])}</span>
|
||||
<span className="text-accent-a d-inline-block">
|
||||
{formatMessage(messages['with.site.name'], { siteName: getConfig().SITE_NAME })}
|
||||
<span>
|
||||
{formatMessage(messages['start.learning'])}{' '}
|
||||
<span className="text-accent-a d-inline-block">
|
||||
{formatMessage(messages['with.site.name'], { siteName: getConfig().SITE_NAME })}
|
||||
</span>
|
||||
</span>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
@@ -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 (
|
||||
@@ -16,11 +16,11 @@ const SmallLayout = ({ username }) => {
|
||||
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}>
|
||||
<Image className="logo-small" alt={getConfig().SITE_NAME} src={getConfig().LOGO_URL} />
|
||||
</Hyperlink>
|
||||
<div className="d-flex align-items-center mb-3 mt-3 mr-3">
|
||||
<div className="d-flex align-items-center m-3.5">
|
||||
<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,4 +0,0 @@
|
||||
const IMAGE_LAYOUT = 'image-layout';
|
||||
const DEFAULT_LAYOUT = 'default-layout';
|
||||
|
||||
export { DEFAULT_LAYOUT, IMAGE_LAYOUT };
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React 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';
|
||||
@@ -10,40 +11,24 @@ import {
|
||||
ImageExtraSmallLayout, ImageLargeLayout, ImageMediumLayout, ImageSmallLayout,
|
||||
} from './components/image-layout';
|
||||
import { AuthLargeLayout, AuthMediumLayout, AuthSmallLayout } from './components/welcome-page-layout';
|
||||
import { DEFAULT_LAYOUT, IMAGE_LAYOUT } from './data/constants';
|
||||
|
||||
const BaseContainer = ({ children, showWelcomeBanner, username }) => {
|
||||
const [baseContainerVersion, setBaseContainerVersion] = useState(DEFAULT_LAYOUT);
|
||||
const BaseContainer = ({ children, showWelcomeBanner, fullName }) => {
|
||||
const enableImageLayout = getConfig().ENABLE_IMAGE_LAYOUT;
|
||||
|
||||
useEffect(() => {
|
||||
const initRebrandExperiment = () => {
|
||||
if (window.experiments?.rebrandExperiment) {
|
||||
setBaseContainerVersion(window.experiments?.rebrandExperiment?.variation);
|
||||
} else {
|
||||
window.experiments = window.experiments || {};
|
||||
window.experiments.rebrandExperiment = {};
|
||||
window.experiments.rebrandExperiment.handleLoaded = () => {
|
||||
setBaseContainerVersion(window.experiments?.rebrandExperiment?.variation);
|
||||
};
|
||||
}
|
||||
};
|
||||
initRebrandExperiment();
|
||||
}, []);
|
||||
|
||||
if (baseContainerVersion === IMAGE_LAYOUT) {
|
||||
if (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 +42,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 +60,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,32 +13,30 @@ 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', () => {
|
||||
window.experiments = {
|
||||
rebrandExperiment: {
|
||||
variation: 'image-layout',
|
||||
},
|
||||
};
|
||||
it('renders Image layout when ENABLE_IMAGE_LAYOUT configuration is enabled', () => {
|
||||
mergeConfig({
|
||||
ENABLE_IMAGE_LAYOUT: true,
|
||||
});
|
||||
|
||||
const baseContainer = mount(
|
||||
const { container } = render(
|
||||
<IntlProvider locale="en">
|
||||
<BaseContainer />
|
||||
<BaseContainer showWelcomeBanner={false} />
|
||||
</IntlProvider>,
|
||||
LargeScreen,
|
||||
);
|
||||
|
||||
expect(baseContainer.find('.banner__image').exists()).toBeTruthy();
|
||||
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';
|
||||
@@ -19,7 +19,8 @@ import { LOGIN_PAGE, SUPPORTED_ICON_CLASSES } from '../data/constants';
|
||||
const EnterpriseSSO = (props) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const tpaProvider = props.provider;
|
||||
const disablePublicAccountCreation = getConfig().ALLOW_PUBLIC_ACCOUNT_CREATION === false;
|
||||
const hideRegistrationLink = getConfig().ALLOW_PUBLIC_ACCOUNT_CREATION === false
|
||||
|| getConfig().SHOW_REGISTRATION_LINKS === false;
|
||||
|
||||
const handleSubmit = (e, url) => {
|
||||
e.preventDefault();
|
||||
@@ -74,7 +75,7 @@ const EnterpriseSSO = (props) => {
|
||||
className="w-100"
|
||||
onClick={(e) => handleClick(e)}
|
||||
>
|
||||
{disablePublicAccountCreation
|
||||
{hideRegistrationLink
|
||||
? formatMessage(messages['enterprisetpa.login.button.text.public.account.creation.disabled'])
|
||||
: formatMessage(messages['enterprisetpa.login.button.text'])}
|
||||
</Button>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
AUTHN_PROGRESSIVE_PROFILING, RECOMMENDATIONS,
|
||||
AUTHN_PROGRESSIVE_PROFILING, RECOMMENDATIONS, REDIRECT,
|
||||
} from '../data/constants';
|
||||
import { setCookie } from '../data/utils';
|
||||
|
||||
@@ -18,6 +18,8 @@ const RedirectLogistration = (props) => {
|
||||
redirectToRecommendationsPage,
|
||||
educationLevel,
|
||||
userId,
|
||||
registrationEmbedded,
|
||||
host,
|
||||
} = props;
|
||||
let finalRedirectUrl = '';
|
||||
|
||||
@@ -37,6 +39,13 @@ const RedirectLogistration = (props) => {
|
||||
// TODO: Do we still need this cookie?
|
||||
setCookie('van-504-returning-user', true);
|
||||
|
||||
if (registrationEmbedded) {
|
||||
window.parent.postMessage({
|
||||
action: REDIRECT,
|
||||
redirectUrl: getConfig().POST_REGISTRATION_REDIRECT_URL,
|
||||
}, host);
|
||||
return null;
|
||||
}
|
||||
const registrationResult = { redirectUrl: finalRedirectUrl, success };
|
||||
return (
|
||||
<Navigate
|
||||
@@ -83,6 +92,8 @@ RedirectLogistration.defaultProps = {
|
||||
optionalFields: {},
|
||||
redirectToRecommendationsPage: false,
|
||||
userId: null,
|
||||
registrationEmbedded: false,
|
||||
host: '',
|
||||
};
|
||||
|
||||
RedirectLogistration.propTypes = {
|
||||
@@ -95,6 +106,8 @@ RedirectLogistration.propTypes = {
|
||||
optionalFields: PropTypes.shape({}),
|
||||
redirectToRecommendationsPage: PropTypes.bool,
|
||||
userId: PropTypes.number,
|
||||
registrationEmbedded: PropTypes.bool,
|
||||
host: PropTypes.string,
|
||||
};
|
||||
|
||||
export default RedirectLogistration;
|
||||
|
||||
@@ -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,23 @@ 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 classNames from 'classnames';
|
||||
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,33 +26,59 @@ 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={classNames(
|
||||
'btn btn-link btn-sm text-body p-0',
|
||||
{ 'mb-0': thirdPartyAuthApiStatus === PENDING_STATE },
|
||||
{ 'mb-4': thirdPartyAuthApiStatus !== PENDING_STATE },
|
||||
)}
|
||||
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} />
|
||||
<div className="mt-4">
|
||||
<Skeleton className="tpa-skeleton" height={36} count={2} />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{(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 +91,8 @@ ThirdPartyAuth.defaultProps = {
|
||||
currentProvider: null,
|
||||
providers: [],
|
||||
secondaryProviders: [],
|
||||
thirdPartyAuthApiStatus: 'pending',
|
||||
thirdPartyAuthApiStatus: PENDING_STATE,
|
||||
isLoginPage: false,
|
||||
};
|
||||
|
||||
ThirdPartyAuth.propTypes = {
|
||||
@@ -86,6 +119,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';
|
||||
|
||||
@@ -2,9 +2,7 @@ import { THIRD_PARTY_AUTH_CONTEXT, THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG } fr
|
||||
import { COMPLETE_STATE, FAILURE_STATE, PENDING_STATE } from '../../data/constants';
|
||||
|
||||
export const defaultState = {
|
||||
fieldDescriptions: {
|
||||
fields: {},
|
||||
},
|
||||
fieldDescriptions: {},
|
||||
optionalFields: {
|
||||
fields: {},
|
||||
extended_profile: [],
|
||||
@@ -33,7 +31,7 @@ const reducer = (state = defaultState, action = {}) => {
|
||||
case THIRD_PARTY_AUTH_CONTEXT.SUCCESS: {
|
||||
return {
|
||||
...state,
|
||||
fieldDescriptions: action.payload.fieldDescriptions.fields,
|
||||
fieldDescriptions: action.payload.fieldDescriptions?.fields,
|
||||
optionalFields: action.payload.optionalFields,
|
||||
thirdPartyAuthContext: action.payload.thirdPartyAuthContext,
|
||||
thirdPartyAuthApiStatus: COMPLETE_STATE,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { mount } from 'enzyme';
|
||||
import { render } from '@testing-library/react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
|
||||
import { REGISTER_EMBEDDED_PAGE } from '../../data/constants';
|
||||
@@ -47,10 +47,13 @@ describe('EmbeddedRegistrationRoute', () => {
|
||||
let embeddedRegistrationPage = null;
|
||||
|
||||
await act(async () => {
|
||||
embeddedRegistrationPage = await mount(routerWrapper());
|
||||
const { container } = await render(routerWrapper());
|
||||
embeddedRegistrationPage = container;
|
||||
});
|
||||
|
||||
expect(embeddedRegistrationPage.find('span').exists()).toBeFalsy();
|
||||
const spanElement = embeddedRegistrationPage.querySelector('span');
|
||||
|
||||
expect(spanElement).toBeNull();
|
||||
});
|
||||
|
||||
it('should render embedded register page if host query param is available in the url (embedded)', async () => {
|
||||
@@ -63,10 +66,13 @@ describe('EmbeddedRegistrationRoute', () => {
|
||||
let embeddedRegistrationPage = null;
|
||||
|
||||
await act(async () => {
|
||||
embeddedRegistrationPage = await mount(routerWrapper());
|
||||
const { container } = await render(routerWrapper());
|
||||
embeddedRegistrationPage = container;
|
||||
});
|
||||
|
||||
expect(embeddedRegistrationPage.find('span').exists()).toBeTruthy();
|
||||
expect(embeddedRegistrationPage.find('span').text()).toBe('Embedded Register Page');
|
||||
const spanElement = embeddedRegistrationPage.querySelector('span');
|
||||
|
||||
expect(spanElement).toBeTruthy();
|
||||
expect(spanElement.textContent).toBe('Embedded Register Page');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 { act } from 'react-dom/test-utils';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import configureStore from 'redux-mock-store';
|
||||
@@ -21,11 +21,17 @@ describe('FormGroup', () => {
|
||||
};
|
||||
|
||||
it('should show help text on field focus', () => {
|
||||
const formGroup = mount(<FormGroup {...props} />);
|
||||
expect(formGroup.find('.pgn-transition-replace-group').find('div#email-1').exists()).toBeFalsy();
|
||||
const { queryByText, getByLabelText } = render(<FormGroup {...props} />);
|
||||
const emailInput = getByLabelText('Email');
|
||||
|
||||
formGroup.find('input#email').simulate('focus');
|
||||
expect(formGroup.find('.pgn-transition-replace-group').find('div#email-1').text()).toEqual('Email field help text');
|
||||
expect(queryByText('Email field help text')).toBeNull();
|
||||
|
||||
fireEvent.focus(emailInput);
|
||||
|
||||
const helpText = queryByText('Email field help text');
|
||||
|
||||
expect(helpText).toBeTruthy();
|
||||
expect(helpText.textContent).toEqual('Email field help text');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -60,25 +66,29 @@ describe('PasswordField', () => {
|
||||
});
|
||||
|
||||
it('should show/hide password on icon click', () => {
|
||||
const passwordField = mount(reduxWrapper(<IntlPasswordField {...props} />));
|
||||
const { getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />));
|
||||
const passwordInput = getByLabelText('Password');
|
||||
|
||||
passwordField.find('button[aria-label="Show password"]').simulate('click');
|
||||
expect(passwordField.find('input').prop('type')).toEqual('text');
|
||||
const showPasswordButton = getByLabelText('Show password');
|
||||
fireEvent.click(showPasswordButton);
|
||||
expect(passwordInput.type).toBe('text');
|
||||
|
||||
passwordField.find('button[aria-label="Hide password"]').simulate('click');
|
||||
expect(passwordField.find('input').prop('type')).toEqual('password');
|
||||
const hidePasswordButton = getByLabelText('Hide password');
|
||||
fireEvent.click(hidePasswordButton);
|
||||
expect(passwordInput.type).toBe('password');
|
||||
});
|
||||
|
||||
it('should show password requirement tooltip on focus', async () => {
|
||||
const passwordField = mount(reduxWrapper(<IntlPasswordField {...props} />));
|
||||
const { getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />));
|
||||
const passwordInput = getByLabelText('Password');
|
||||
jest.useFakeTimers();
|
||||
await act(async () => {
|
||||
passwordField.find('input').simulate('focus');
|
||||
fireEvent.focus(passwordInput);
|
||||
jest.runAllTimers();
|
||||
});
|
||||
passwordField.update();
|
||||
const passwordRequirementTooltip = document.querySelector('#password-requirement-left');
|
||||
|
||||
expect(passwordField.find('#password-requirement-left').exists()).toBeTruthy();
|
||||
expect(passwordRequirementTooltip).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should show all password requirement checks as failed', async () => {
|
||||
@@ -86,48 +96,66 @@ describe('PasswordField', () => {
|
||||
...props,
|
||||
value: '',
|
||||
};
|
||||
|
||||
const { getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />));
|
||||
const passwordInput = getByLabelText('Password');
|
||||
jest.useFakeTimers();
|
||||
const passwordField = mount(reduxWrapper(<IntlPasswordField {...props} />));
|
||||
await act(async () => {
|
||||
passwordField.find('input').simulate('focus');
|
||||
fireEvent.focus(passwordInput);
|
||||
jest.runAllTimers();
|
||||
});
|
||||
passwordField.update();
|
||||
|
||||
expect(passwordField.find('#letter-check span').prop('className')).toEqual('pgn__icon mr-1 text-light-700');
|
||||
expect(passwordField.find('#number-check span').prop('className')).toEqual('pgn__icon mr-1 text-light-700');
|
||||
expect(passwordField.find('#characters-check span').prop('className')).toEqual('pgn__icon mr-1 text-light-700');
|
||||
const letterCheckIcon = document.querySelector('#letter-check span');
|
||||
const numberCheckIcon = document.querySelector('#number-check span');
|
||||
const charactersCheckIcon = document.querySelector('#characters-check span');
|
||||
|
||||
expect(letterCheckIcon).toBeTruthy();
|
||||
expect(letterCheckIcon.className).toContain('pgn__icon mr-1 text-light-700');
|
||||
|
||||
expect(numberCheckIcon).toBeTruthy();
|
||||
expect(numberCheckIcon.className).toContain('pgn__icon mr-1 text-light-700');
|
||||
|
||||
expect(charactersCheckIcon).toBeTruthy();
|
||||
expect(charactersCheckIcon.className).toContain('pgn__icon mr-1 text-light-700');
|
||||
});
|
||||
|
||||
it('should update password requirement checks', async () => {
|
||||
const passwordField = mount(reduxWrapper(<IntlPasswordField {...props} />));
|
||||
const { getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />));
|
||||
const passwordInput = getByLabelText('Password');
|
||||
jest.useFakeTimers();
|
||||
await act(async () => {
|
||||
passwordField.find('input').simulate('focus');
|
||||
fireEvent.focus(passwordInput);
|
||||
jest.runAllTimers();
|
||||
});
|
||||
passwordField.update();
|
||||
|
||||
expect(passwordField.find('#letter-check span').prop('className')).toEqual('pgn__icon text-success mr-1');
|
||||
expect(passwordField.find('#number-check span').prop('className')).toEqual('pgn__icon text-success mr-1');
|
||||
expect(passwordField.find('#characters-check span').prop('className')).toEqual('pgn__icon text-success mr-1');
|
||||
const letterCheckIcon = document.querySelector('#letter-check span');
|
||||
const numberCheckIcon = document.querySelector('#number-check span');
|
||||
const charactersCheckIcon = document.querySelector('#characters-check span');
|
||||
|
||||
expect(letterCheckIcon).toBeTruthy();
|
||||
expect(letterCheckIcon.className).toContain('pgn__icon text-success mr-1');
|
||||
|
||||
expect(numberCheckIcon).toBeTruthy();
|
||||
expect(numberCheckIcon.className).toContain('pgn__icon text-success mr-1');
|
||||
|
||||
expect(charactersCheckIcon).toBeTruthy();
|
||||
expect(charactersCheckIcon.className).toContain('pgn__icon text-success mr-1');
|
||||
});
|
||||
|
||||
it('should not run validations when blur is fired on password icon click', () => {
|
||||
const passwordField = mount(reduxWrapper(<IntlPasswordField {...props} />));
|
||||
const { container, getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />));
|
||||
const passwordInput = container.querySelector('input[name="password"]');
|
||||
|
||||
passwordField.find('button[aria-label="Show password"]').simulate('blur', {
|
||||
const passwordIcon = getByLabelText('Show password');
|
||||
|
||||
fireEvent.blur(passwordInput, {
|
||||
target: {
|
||||
name: 'password',
|
||||
value: 'invalid',
|
||||
},
|
||||
relatedTarget: {
|
||||
name: 'passwordIcon',
|
||||
},
|
||||
relatedTarget: passwordIcon,
|
||||
});
|
||||
|
||||
expect(passwordField.find('div[feedback-for="password"]').exists()).toBeFalsy();
|
||||
expect(container.querySelector('div[feedback-for="password"]')).toBeNull();
|
||||
});
|
||||
|
||||
it('should call props handle blur if available', () => {
|
||||
@@ -135,9 +163,10 @@ describe('PasswordField', () => {
|
||||
...props,
|
||||
handleBlur: jest.fn(),
|
||||
};
|
||||
const passwordField = mount(reduxWrapper(<IntlPasswordField {...props} />));
|
||||
const { container } = render(reduxWrapper(<IntlPasswordField {...props} />));
|
||||
const passwordInput = container.querySelector('input[name="password"]');
|
||||
|
||||
passwordField.find('input#password').simulate('blur', {
|
||||
fireEvent.blur(passwordInput, {
|
||||
target: {
|
||||
name: 'password',
|
||||
value: '',
|
||||
@@ -152,9 +181,10 @@ describe('PasswordField', () => {
|
||||
...props,
|
||||
handleErrorChange: jest.fn(),
|
||||
};
|
||||
const passwordField = mount(reduxWrapper(<IntlPasswordField {...props} />));
|
||||
const { container } = render(reduxWrapper(<IntlPasswordField {...props} />));
|
||||
const passwordInput = container.querySelector('input[name="password"]');
|
||||
|
||||
passwordField.find('input#password').simulate('blur', {
|
||||
fireEvent.blur(passwordInput, {
|
||||
target: {
|
||||
name: 'password',
|
||||
value: '',
|
||||
@@ -174,9 +204,11 @@ describe('PasswordField', () => {
|
||||
handleErrorChange: jest.fn(),
|
||||
};
|
||||
|
||||
const passwordField = mount(reduxWrapper(<IntlPasswordField {...props} />));
|
||||
const { getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />));
|
||||
|
||||
passwordField.find('button[aria-label="Show password"]').simulate('focus', {
|
||||
const passwordIcon = getByLabelText('Show password');
|
||||
|
||||
fireEvent.focus(passwordIcon, {
|
||||
target: {
|
||||
name: 'passwordIcon',
|
||||
value: '',
|
||||
@@ -192,9 +224,11 @@ describe('PasswordField', () => {
|
||||
handleErrorChange: jest.fn(),
|
||||
};
|
||||
|
||||
const passwordField = mount(reduxWrapper(<IntlPasswordField {...props} />));
|
||||
const { getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />));
|
||||
|
||||
passwordField.find('button[aria-label="Show password"]').simulate('focus', {
|
||||
const passwordIcon = getByLabelText('Show password');
|
||||
|
||||
fireEvent.focus(passwordIcon, {
|
||||
target: {
|
||||
name: 'password',
|
||||
value: 'invalid',
|
||||
@@ -214,9 +248,9 @@ describe('PasswordField', () => {
|
||||
...props,
|
||||
handleErrorChange: jest.fn(),
|
||||
};
|
||||
const passwordField = mount(reduxWrapper(<IntlPasswordField {...props} />));
|
||||
|
||||
passwordField.find('button[aria-label="Show password"]').simulate('blur', {
|
||||
const { getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />));
|
||||
const passwordField = getByLabelText('Password');
|
||||
fireEvent.blur(passwordField, {
|
||||
target: {
|
||||
name: 'password',
|
||||
value: 'password123',
|
||||
@@ -234,9 +268,11 @@ describe('PasswordField', () => {
|
||||
handleErrorChange: jest.fn(),
|
||||
handleBlur: jest.fn(),
|
||||
};
|
||||
const passwordField = mount(reduxWrapper(<IntlPasswordField {...props} />));
|
||||
const { getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />));
|
||||
|
||||
passwordField.find('button[aria-label="Show password"]').simulate('blur', {
|
||||
const passwordIcon = getByLabelText('Show password');
|
||||
|
||||
fireEvent.blur(passwordIcon, {
|
||||
target: {
|
||||
name: 'passwordIcon',
|
||||
value: undefined,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import { fetchAuthenticatedUser, getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import { mount } from 'enzyme';
|
||||
import { render } from '@testing-library/react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
|
||||
import { UnAuthOnlyRoute } from '..';
|
||||
@@ -55,7 +55,7 @@ describe('UnAuthOnlyRoute', () => {
|
||||
fetchAuthenticatedUser.mockReturnValueOnce(Promise.resolve(user));
|
||||
|
||||
await act(async () => {
|
||||
await mount(routerWrapper());
|
||||
await render(routerWrapper());
|
||||
});
|
||||
|
||||
expect(fetchAuthenticatedUser).toBeCalledWith({ forceRefresh: true });
|
||||
@@ -66,7 +66,7 @@ describe('UnAuthOnlyRoute', () => {
|
||||
fetchAuthenticatedUser.mockReturnValueOnce(Promise.resolve(null));
|
||||
|
||||
await act(async () => {
|
||||
await mount(routerWrapper());
|
||||
await render(routerWrapper());
|
||||
});
|
||||
|
||||
expect(fetchAuthenticatedUser).toBeCalledWith({ forceRefresh: false });
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
const configuration = {
|
||||
// Cookies related configs
|
||||
SESSION_COOKIE_DOMAIN: process.env.SESSION_COOKIE_DOMAIN,
|
||||
REGISTER_CONVERSION_COOKIE_NAME: process.env.REGISTER_CONVERSION_COOKIE_NAME || null,
|
||||
USER_RETENTION_COOKIE_NAME: process.env.USER_RETENTION_COOKIE_NAME || '',
|
||||
// Features
|
||||
DISABLE_ENTERPRISE_LOGIN: process.env.DISABLE_ENTERPRISE_LOGIN || '',
|
||||
ENABLE_AUTO_GENERATED_USERNAME: process.env.ENABLE_AUTO_GENERATED_USERNAME || true,
|
||||
ENABLE_DYNAMIC_REGISTRATION_FIELDS: process.env.ENABLE_DYNAMIC_REGISTRATION_FIELDS || false,
|
||||
ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN: process.env.ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN || false,
|
||||
ENABLE_POST_REGISTRATION_RECOMMENDATIONS: process.env.ENABLE_POST_REGISTRATION_RECOMMENDATIONS || false,
|
||||
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,
|
||||
|
||||
52
src/data/tests/cookies.test.js
Normal file
52
src/data/tests/cookies.test.js
Normal file
@@ -0,0 +1,52 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import Cookies from 'universal-cookie';
|
||||
|
||||
import { setCookie } from '../utils';
|
||||
|
||||
// Mock getConfig function
|
||||
jest.mock('@edx/frontend-platform', () => ({
|
||||
getConfig: jest.fn(),
|
||||
}));
|
||||
|
||||
// Mock Cookies class
|
||||
jest.mock('universal-cookie');
|
||||
|
||||
describe('setCookie function', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should set a cookie with default options', () => {
|
||||
getConfig.mockReturnValue({ SESSION_COOKIE_DOMAIN: 'example.com' });
|
||||
|
||||
setCookie('testCookie', 'testValue');
|
||||
|
||||
expect(Cookies).toHaveBeenCalled();
|
||||
expect(Cookies).toHaveBeenCalledWith();
|
||||
expect(Cookies.prototype.set).toHaveBeenCalledWith('testCookie', 'testValue', {
|
||||
domain: 'example.com',
|
||||
path: '/',
|
||||
});
|
||||
});
|
||||
|
||||
it('should set a cookie with specified expiry', () => {
|
||||
getConfig.mockReturnValue({ SESSION_COOKIE_DOMAIN: 'example.com' });
|
||||
|
||||
const expiry = new Date('2023-12-31');
|
||||
setCookie('testCookie', 'testValue', expiry);
|
||||
|
||||
expect(Cookies).toHaveBeenCalled();
|
||||
expect(Cookies).toHaveBeenCalledWith();
|
||||
expect(Cookies.prototype.set).toHaveBeenCalledWith('testCookie', 'testValue', {
|
||||
domain: 'example.com',
|
||||
path: '/',
|
||||
expires: expiry,
|
||||
});
|
||||
});
|
||||
|
||||
it('should not set a cookie if cookieName is undefined', () => {
|
||||
setCookie(undefined, 'testValue');
|
||||
|
||||
expect(Cookies).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -2,10 +2,12 @@ import { getConfig } from '@edx/frontend-platform';
|
||||
import Cookies from 'universal-cookie';
|
||||
|
||||
export default function setCookie(cookieName, cookieValue, cookieExpiry) {
|
||||
const cookies = new Cookies();
|
||||
const options = { domain: getConfig().SESSION_COOKIE_DOMAIN, path: '/' };
|
||||
if (cookieExpiry) {
|
||||
options.expires = cookieExpiry;
|
||||
if (cookieName) { // To avoid setting getting exception when setting cookie with undefined names.
|
||||
const cookies = new Cookies();
|
||||
const options = { domain: getConfig().SESSION_COOKIE_DOMAIN, path: '/' };
|
||||
if (cookieExpiry) {
|
||||
options.expires = cookieExpiry;
|
||||
}
|
||||
cookies.set(cookieName, cookieValue, options);
|
||||
}
|
||||
cookies.set(cookieName, cookieValue, options);
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -3,8 +3,9 @@ import { Provider } from 'react-redux';
|
||||
|
||||
import { mergeConfig } from '@edx/frontend-platform';
|
||||
import { configure, injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { mount } from 'enzyme';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import {
|
||||
fireEvent, render, screen,
|
||||
} from '@testing-library/react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import configureStore from 'redux-mock-store';
|
||||
|
||||
@@ -72,31 +73,39 @@ describe('ForgotPasswordPage', () => {
|
||||
status: null,
|
||||
};
|
||||
});
|
||||
const findByTextContent = (container, text) => Array.from(container.querySelectorAll('*')).find(
|
||||
element => element.textContent === text,
|
||||
);
|
||||
|
||||
it('not should display need other help signing in button', () => {
|
||||
const wrapper = mount(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||
expect(wrapper.find('#forgot-password').exists()).toBeFalsy();
|
||||
const { queryByTestId } = render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||
const forgotPasswordButton = queryByTestId('forgot-password');
|
||||
expect(forgotPasswordButton).toBeNull();
|
||||
});
|
||||
|
||||
it('should display need other help signing in button', () => {
|
||||
mergeConfig({
|
||||
LOGIN_ISSUE_SUPPORT_LINK: '/support',
|
||||
});
|
||||
const wrapper = mount(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||
expect(wrapper.find('#forgot-password').first().text()).toEqual('Need help signing in?');
|
||||
render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||
const forgotPasswordButton = screen.findByText('Need help signing in?');
|
||||
expect(forgotPasswordButton).toBeDefined();
|
||||
});
|
||||
|
||||
it('should display email validation error message', async () => {
|
||||
const validationMessage = 'We were unable to contact you.Enter a valid email address below.';
|
||||
const wrapper = mount(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||
const { container } = render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||
|
||||
wrapper.find('input#email').simulate(
|
||||
'change', { target: { value: 'invalid-email', name: 'email' } },
|
||||
);
|
||||
await act(async () => { await wrapper.find('button.btn-brand').simulate('click'); });
|
||||
wrapper.update();
|
||||
const emailInput = screen.getByLabelText('Email');
|
||||
|
||||
expect(wrapper.find('.alert-danger').text()).toEqual(validationMessage);
|
||||
fireEvent.change(emailInput, { target: { value: 'invalid-email' } });
|
||||
|
||||
const submitButton = screen.getByText('Submit');
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
const alertElements = container.querySelectorAll('.alert-danger');
|
||||
const validationErrors = alertElements[0].textContent;
|
||||
expect(validationErrors).toBe(validationMessage);
|
||||
});
|
||||
|
||||
it('should show alert on server error', () => {
|
||||
@@ -105,19 +114,25 @@ describe('ForgotPasswordPage', () => {
|
||||
});
|
||||
const expectedMessage = 'We were unable to contact you.'
|
||||
+ 'An error has occurred. Try refreshing the page, or check your internet connection.';
|
||||
const wrapper = mount(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||
|
||||
expect(wrapper.find('#validation-errors').first().text()).toEqual(expectedMessage);
|
||||
const { container } = render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||
|
||||
const alertElements = container.querySelectorAll('.alert-danger');
|
||||
const validationErrors = alertElements[0].textContent;
|
||||
expect(validationErrors).toBe(expectedMessage);
|
||||
});
|
||||
|
||||
it('should display empty email validation message', async () => {
|
||||
const validationMessage = 'We were unable to contact you.Enter your email below.';
|
||||
const forgotPasswordPage = mount(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||
const { container } = render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||
|
||||
await act(async () => { await forgotPasswordPage.find('button.btn-brand').simulate('click'); });
|
||||
const submitButton = screen.getByText('Submit');
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
forgotPasswordPage.update();
|
||||
expect(forgotPasswordPage.find('.alert-danger').text()).toEqual(validationMessage);
|
||||
const alertElements = container.querySelectorAll('.alert-danger');
|
||||
const validationErrors = alertElements[0].textContent;
|
||||
|
||||
expect(validationErrors).toBe(validationMessage);
|
||||
});
|
||||
|
||||
it('should display request in progress error message', () => {
|
||||
@@ -126,18 +141,22 @@ describe('ForgotPasswordPage', () => {
|
||||
forgotPassword: { status: 'forbidden' },
|
||||
});
|
||||
|
||||
const forgotPasswordPage = mount(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||
expect(forgotPasswordPage.find('.alert-danger').text()).toEqual(rateLimitMessage);
|
||||
const { container } = render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||
|
||||
const alertElements = container.querySelectorAll('.alert-danger');
|
||||
const validationErrors = alertElements[0].textContent;
|
||||
expect(validationErrors).toBe(rateLimitMessage);
|
||||
});
|
||||
|
||||
it('should not display any error message on change event', () => {
|
||||
const forgotPasswordPage = mount(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||
render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||
|
||||
const emailInput = forgotPasswordPage.find('input#email');
|
||||
emailInput.simulate('change', { target: { value: 'invalid-email', name: 'email' } });
|
||||
forgotPasswordPage.update();
|
||||
const emailInput = screen.getByLabelText('Email');
|
||||
|
||||
expect(forgotPasswordPage.find('#email-invalid-feedback').exists()).toEqual(false);
|
||||
fireEvent.change(emailInput, { target: { value: 'invalid-email' } });
|
||||
|
||||
const errorElement = screen.queryByTestId('email-invalid-feedback');
|
||||
expect(errorElement).toBeNull();
|
||||
});
|
||||
|
||||
it('should set error in redux store on onBlur', () => {
|
||||
@@ -153,8 +172,11 @@ describe('ForgotPasswordPage', () => {
|
||||
};
|
||||
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
const forgotPasswordPage = mount(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||
forgotPasswordPage.find('input#email').simulate('blur');
|
||||
render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||
const emailInput = screen.getByLabelText('Email');
|
||||
|
||||
fireEvent.blur(emailInput);
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith(setForgotPasswordFormData(forgotPasswordFormData));
|
||||
});
|
||||
|
||||
@@ -165,9 +187,9 @@ describe('ForgotPasswordPage', () => {
|
||||
emailValidationError: validationMessage,
|
||||
email: '',
|
||||
};
|
||||
const forgotPasswordPage = mount(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||
forgotPasswordPage.update();
|
||||
expect(forgotPasswordPage.find('.pgn__form-text-invalid').text()).toEqual(validationMessage);
|
||||
const { container } = render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||
const validationElement = container.querySelector('.pgn__form-text-invalid');
|
||||
expect(validationElement.textContent).toEqual(validationMessage);
|
||||
});
|
||||
|
||||
it('should clear error in redux store on onFocus', () => {
|
||||
@@ -182,8 +204,12 @@ describe('ForgotPasswordPage', () => {
|
||||
};
|
||||
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
const forgotPasswordPage = mount(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||
forgotPasswordPage.find('input#email').simulate('focus');
|
||||
|
||||
render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||
const emailInput = screen.getByLabelText('Email');
|
||||
|
||||
fireEvent.focus(emailInput);
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith(setForgotPasswordFormData(forgotPasswordFormData));
|
||||
});
|
||||
|
||||
@@ -193,9 +219,9 @@ describe('ForgotPasswordPage', () => {
|
||||
emailValidationError: '',
|
||||
email: '',
|
||||
};
|
||||
const forgotPasswordPage = mount(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||
forgotPasswordPage.update();
|
||||
expect(forgotPasswordPage.find('#email-invalid-feedback').exists()).toEqual(false);
|
||||
render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||
const errorElement = screen.queryByTestId('email-invalid-feedback');
|
||||
expect(errorElement).toBeNull();
|
||||
});
|
||||
|
||||
it('should display success message after email is sent', () => {
|
||||
@@ -205,12 +231,16 @@ describe('ForgotPasswordPage', () => {
|
||||
status: 'complete',
|
||||
},
|
||||
});
|
||||
|
||||
const successMessage = 'Check your emailWe sent an email to with instructions to reset your password. If you do not '
|
||||
+ 'receive a password reset message after 1 minute, verify that you entered the correct email address,'
|
||||
+ ' or check your spam folder. If you need further assistance, contact technical support.';
|
||||
|
||||
const wrapper = mount(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||
expect(wrapper.find('.alert-success').text()).toEqual(successMessage);
|
||||
const { container } = render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||
const successElement = findByTextContent(container, successMessage);
|
||||
|
||||
expect(successElement).toBeDefined();
|
||||
expect(successElement.textContent).toEqual(successMessage);
|
||||
});
|
||||
|
||||
it('should display invalid password reset link error', () => {
|
||||
@@ -224,16 +254,20 @@ describe('ForgotPasswordPage', () => {
|
||||
+ 'This password reset link is invalid. It may have been used already. '
|
||||
+ 'Enter your email below to receive a new link.';
|
||||
|
||||
const wrapper = mount(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||
expect(wrapper.find('.alert-danger').text()).toEqual(successMessage);
|
||||
const { container } = render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||
const successElement = findByTextContent(container, successMessage);
|
||||
|
||||
expect(successElement).toBeDefined();
|
||||
expect(successElement.textContent).toEqual(successMessage);
|
||||
});
|
||||
|
||||
it('should redirect onto login page', async () => {
|
||||
const forgotPasswordPage = mount(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||
const { container } = render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||
|
||||
await act(async () => { await forgotPasswordPage.find('nav').find('a').first().simulate('click'); });
|
||||
const navElement = container.querySelector('nav');
|
||||
const anchorElement = navElement.querySelector('a');
|
||||
fireEvent.click(anchorElement);
|
||||
|
||||
forgotPasswordPage.update();
|
||||
expect(mockedNavigator).toHaveBeenCalledWith(LOGIN_PAGE);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,11 +1,5 @@
|
||||
{
|
||||
"start.learning": "ابدأ التعلم ",
|
||||
"with.site.name": "مع {siteName}",
|
||||
"your.career.turning.point": "Your career turning point",
|
||||
"is.here": "is here.",
|
||||
"welcome.to.platform": "أهلا بك {username} في {siteName}",
|
||||
"complete.your.profile.1": "أكمل",
|
||||
"complete.your.profile.2": "ملفك الشخصي",
|
||||
"error.notfound.message": "الصفحة التي تبحث عنها غير متوفرة أو هناك خطأ في العنوان. رجاءً تحقق من العنوان و حاول مجددًا.",
|
||||
"institution.login.page.sub.heading": "اختر مؤسستك من القائمة أدناه",
|
||||
"logistration.sign.in": "تسجيل الدخول",
|
||||
"logistration.register": "التسجيل",
|
||||
@@ -26,8 +20,7 @@
|
||||
"registration.using.tpa.form.heading": "إتمام إنشاء حسابك",
|
||||
"zendesk.supportTitle": "edX Support",
|
||||
"zendesk.selectTicketForm": "الرجاء اختيار نوع الطلب الخاص بك:",
|
||||
"error.notfound.message": "الصفحة التي تبحث عنها غير متوفرة أو هناك خطأ في العنوان. رجاءً تحقق من العنوان و حاول مجددًا.",
|
||||
"forgot.password.confirmation.message": "لقد أرسلنا بريدًا إلكترونيًا إلى {email} به إرشادات لإعادة ضبط كلمة المرور الخاصة بك. إن لم تستلم رسالة إعادة ضبط كلمة المرور بعد دقيقة واحدة، فتحقق من إدخال عنوان البريد الإلكتروني الصحيح، أو تفقد مجلد الرسائل غير المرغوب فيها. إن احتجت مزيدًا من المساعدة، {supportLink}.",
|
||||
"forgot.password.confirmation.message": "We sent an email to {email} with instructions to reset your password. If you do not receive a password reset message after 1 minute, verify that you entered the correct email address, or check your spam folder. If you need further assistance, {supportLink}.",
|
||||
"forgot.password.page.title": "نسيت كلمة المرور | {siteName}",
|
||||
"forgot.password.page.heading": "إعادة ضبط كلمة المرور",
|
||||
"forgot.password.page.instructions": "رجاءً أدخل عنوان بريدك الإلكتروني أدناه وسنرسل إليك بريدًا به إرشادات بخصوص كيفية إعادة ضبط كلمة مرورك.",
|
||||
@@ -42,7 +35,7 @@
|
||||
"confirmation.message.title": "تفقّد بريدك الإلكتروني",
|
||||
"confirmation.support.link": "اتصل بالدعم الفني",
|
||||
"need.help.sign.in.text": "هل تحتاج مساعدة في تسجيل الدخول؟",
|
||||
"additional.help.text": "للمزيد من المساعدة، اتصل بدعم {platformName} على ",
|
||||
"additional.help.text": "For additional help, contact {platformName} support at",
|
||||
"sign.in.text": "تسجيل الدخول",
|
||||
"extend.field.errors": "{emailError} أدناه.",
|
||||
"invalid.token.heading": "رابط إعادة ضبط كلمة المرور غير صالح",
|
||||
@@ -53,9 +46,9 @@
|
||||
"token.validation.internal.sever.error": "حدث خطأ ما. جرب تحديث الصفحة أو تحقق من اتصالك بالإنترنت.",
|
||||
"internal.server.error": "حدث خطأ ما. جرب تحديث الصفحة أو تحقق من اتصالك بالإنترنت.",
|
||||
"account.activation.error.message": "شي ما لم يسر على ما يرام، يرجى {supportLink} لحل هذه المشكلة.",
|
||||
"login.inactive.user.error": "أنت بحاجة لتفعيل حسابك من أجل تسجيل الدخول{lineBreak}\n{lineBreak}لقد أرسلنا للتو رابطًا للتفعيل إلى {email}. إن لم تتلقّ بريدًا إلكترونيا، تفقّد مجلدات الرسائل غير المرغوب فيها أو {supportLink}.",
|
||||
"login.inactive.user.error": "In order to sign in, you need to activate your account.{lineBreak} {lineBreak}We just sent an activation link to {email}. If you do not receive an email, check your spam folders or {supportLink}.",
|
||||
"allowed.domain.login.error": "كونك مستخدمًا على {allowedDomain}، فإن عليك تسجيل الدخول باستخدام {tpaLink} الخاص بـ {allowedDomain} .",
|
||||
"login.incorrect.credentials.error.attempts.text.1": "اسم المستخدم أو البريد الإلكتروني أو كلمة المرور التي أدخلتها غير صحيحة. لديك {remainingAttempts, plural,\n one {محاولة واحدة}\n two {محاولتان}\n few {# محاولات}\n many {# محاولة}\n other {# محاولة}\n} أخرى لتسجيل الدخول قبل أن يتم إقفال حسابك مؤقتًا.",
|
||||
"login.incorrect.credentials.error.attempts.text.1": "The username, email or password you entered is incorrect. You have {remainingAttempts} more sign in attempts before your account is temporarily locked.",
|
||||
"login.incorrect.credentials.error.attempts.text.2": "إن نسيت كلمة مرورك، {resetLink}",
|
||||
"account.locked.out.message.2": "لتكون في مأمن، يمكنك {resetLink} قبل تكرار المحاولة.",
|
||||
"login.incorrect.credentials.error.with.reset.link": "اسم المستخدم أو البريد الإلكتروني أو كلمة المرور التي أدخلتها غير صحيحة. يرجى تكرار المحاولة أو {resetLink}.",
|
||||
@@ -71,7 +64,7 @@
|
||||
"non.compliant.password.message": "كلمة مرورك الحالية لا تستسجيب لمتطلبات الأمان الجديدة. لقد أرسلنا للتو رسالة لإعادة ضبط كلمة المرور إلى عنوان البريد الإلكتروني المرتبط بهذا الحساب. شكرًا لك على مساعدتنا في الحفاظ على سلامة بياناتك.",
|
||||
"account.locked.out.message.1": "لحماية حسابك، تم إقفاله مؤقتًا. حاول مرة أخرى بعد 30 دقيقة.",
|
||||
"enterprise.login.btn.text": "بيانات الشركة أو المدرسة",
|
||||
"username.or.email.format.validation.less.chars.message": "يجب أن يحتوي اسم المستخدم أو البريد الإلكتروني على 3 أحرف على الأقل.",
|
||||
"username.or.email.format.validation.less.chars.message": "يجب أن يحتوي اسم المستخدم أو البريد الإلكتروني على 2 أحرف على الأقل.",
|
||||
"email.validation.message": "أدخل اسم المستخدم أو البريد الإلكتروني الخاص بك",
|
||||
"password.validation.message": "لم يتم استيفاء معايير كلمة المرور",
|
||||
"account.activation.success.message.title": "نجح الأمر! لقد قمت بتفعيل حسابك.",
|
||||
@@ -113,8 +106,9 @@
|
||||
"recommendation.page.title": "التوصيات | {siteName}",
|
||||
"recommendation.page.heading": "لدينا بعض التوصيات لكي تبدأ.",
|
||||
"recommendation.skip.button": "التخطي مؤقتا",
|
||||
"recommendation.option.trending": "Trending",
|
||||
"recommendation.option.trending": "Trending Now",
|
||||
"recommendation.option.popular": "Most Popular",
|
||||
"recommendation.option.recommended.for.you": "Recommended For You",
|
||||
"recommendation.product-card.pill-text.course": "Course",
|
||||
"recommendation.product-card.pill-text.professional-certificate": "Professional Certificate",
|
||||
"recommendation.product-card.pill-text.emeritus": "Offered on Emeritus",
|
||||
@@ -160,9 +154,6 @@
|
||||
"terms.of.service": "شروط الخدمة",
|
||||
"registration.username.suggestion.label": "مقترح:",
|
||||
"did.you.mean.alert.text": "هل تقصد",
|
||||
"register.page.terms.of.service.and.honor.code": "بإنشاءك حسابًا، فإنك توافق على {tosAndHonorCode} و تقر بأن {platformName} و كل عضو يعالج بياناتك الشخصية وفقًا لـ{privacyPolicy}.",
|
||||
"register.page.honor.code": "اوافق على شروط {platformName} {tosAndHonorCode}",
|
||||
"register.page.terms.of.service": "اوافق على {platformName} {termsOfService}",
|
||||
"sign.in": "تسجيل الدخول",
|
||||
"reset.password.page.title": "إعادة ضبط كلمة المرور | {siteName}",
|
||||
"reset.password": "إعادة ضبط كلمة المرور",
|
||||
@@ -176,5 +167,15 @@
|
||||
"reset.server.rate.limit.error": "طلبات أكثر مما ينبغي.",
|
||||
"reset.password.success.heading": "تمت إعادة ضبط كلمة المرور.",
|
||||
"reset.password.success": "تمت إعادة ضبط كلمة مرورك. سجل الدخول إلى حسابك.",
|
||||
"rate.limit.error": "حدث خطأ بسبب كثرة الطلبات. رجاءً حاول مرة أخرى بعد مضي بعض الوقت."
|
||||
}
|
||||
"rate.limit.error": "حدث خطأ بسبب كثرة الطلبات. رجاءً حاول مرة أخرى بعد مضي بعض الوقت.",
|
||||
"start.learning": "ابدأ التعلم ",
|
||||
"with.site.name": "مع {siteName}",
|
||||
"your.career.turning.point": "Your career turning point",
|
||||
"is.here": "is here.",
|
||||
"welcome.to.platform": "أهلا بك {username} في {siteName}",
|
||||
"complete.your.profile.1": "أكمل",
|
||||
"complete.your.profile.2": "ملفك الشخصي",
|
||||
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each Member process your personal data in accordance with the {privacyPolicy}.",
|
||||
"register.page.honor.code": "I agree to the {platformName} {tosAndHonorCode}",
|
||||
"register.page.terms.of.service": "I agree to the {platformName} {termsOfService}"
|
||||
}
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
{
|
||||
"start.learning": "Start learning",
|
||||
"with.site.name": "with {siteName}",
|
||||
"your.career.turning.point": "Your career turning point",
|
||||
"is.here": "is here.",
|
||||
"welcome.to.platform": "Welcome to {siteName}, {username}!",
|
||||
"complete.your.profile.1": "Complete",
|
||||
"complete.your.profile.2": "your profile",
|
||||
"error.notfound.message": "The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again.",
|
||||
"institution.login.page.sub.heading": "Choose your institution from the list below",
|
||||
"logistration.sign.in": "Sign in",
|
||||
"logistration.register": "Register",
|
||||
@@ -26,8 +20,7 @@
|
||||
"registration.using.tpa.form.heading": "Finish creating your account",
|
||||
"zendesk.supportTitle": "edX Support",
|
||||
"zendesk.selectTicketForm": "Please choose your request type:",
|
||||
"error.notfound.message": "The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again.",
|
||||
"forgot.password.confirmation.message": "We sent an email to {email} with instructions to reset your password.\n If you do not receive a password reset message after 1 minute, verify that you entered\n the correct email address, or check your spam folder. If you need further assistance, {supportLink}.",
|
||||
"forgot.password.confirmation.message": "We sent an email to {email} with instructions to reset your password. If you do not receive a password reset message after 1 minute, verify that you entered the correct email address, or check your spam folder. If you need further assistance, {supportLink}.",
|
||||
"forgot.password.page.title": "Forgot Password | {siteName}",
|
||||
"forgot.password.page.heading": "Reset password",
|
||||
"forgot.password.page.instructions": "Please enter your email address below and we will send you an email with instructions on how to reset your password.",
|
||||
@@ -42,7 +35,7 @@
|
||||
"confirmation.message.title": "Check your email",
|
||||
"confirmation.support.link": "contact technical support",
|
||||
"need.help.sign.in.text": "Need help signing in?",
|
||||
"additional.help.text": "For additional help, contact {platformName} support at ",
|
||||
"additional.help.text": "For additional help, contact {platformName} support at",
|
||||
"sign.in.text": "Sign in",
|
||||
"extend.field.errors": "{emailError} below.",
|
||||
"invalid.token.heading": "Invalid password reset link",
|
||||
@@ -53,9 +46,9 @@
|
||||
"token.validation.internal.sever.error": "An error has occurred. Try refreshing the page, or check your internet connection.",
|
||||
"internal.server.error": "An error has occurred. Try refreshing the page, or check your internet connection.",
|
||||
"account.activation.error.message": "Something went wrong, please {supportLink} to resolve this issue.",
|
||||
"login.inactive.user.error": "In order to sign in, you need to activate your account.{lineBreak}\n {lineBreak}We just sent an activation link to {email}. If you do not receive an email,\n check your spam folders or {supportLink}.",
|
||||
"login.inactive.user.error": "In order to sign in, you need to activate your account.{lineBreak} {lineBreak}We just sent an activation link to {email}. If you do not receive an email, check your spam folders or {supportLink}.",
|
||||
"allowed.domain.login.error": "As {allowedDomain} user, You must login with your {allowedDomain} {tpaLink}.",
|
||||
"login.incorrect.credentials.error.attempts.text.1": "The username, email or password you entered is incorrect. You have {remainingAttempts} more sign in\n attempts before your account is temporarily locked.",
|
||||
"login.incorrect.credentials.error.attempts.text.1": "The username, email or password you entered is incorrect. You have {remainingAttempts} more sign in attempts before your account is temporarily locked.",
|
||||
"login.incorrect.credentials.error.attempts.text.2": "If you've forgotten your password, {resetLink}",
|
||||
"account.locked.out.message.2": "To be on the safe side, you can {resetLink} before trying again.",
|
||||
"login.incorrect.credentials.error.with.reset.link": "The username, email, or password you entered is incorrect. Please try again or {resetLink}.",
|
||||
@@ -71,7 +64,7 @@
|
||||
"non.compliant.password.message": "Your current password does not meet the new security requirements. We just sent a password-reset message to the email address associated with this account. Thank you for helping us keep your data safe.",
|
||||
"account.locked.out.message.1": "To protect your account, it's been temporarily locked. Try again in 30 minutes.",
|
||||
"enterprise.login.btn.text": "Company or school credentials",
|
||||
"username.or.email.format.validation.less.chars.message": "Username or email must have at least 3 characters.",
|
||||
"username.or.email.format.validation.less.chars.message": "Username or email must have at least 2 characters.",
|
||||
"email.validation.message": "Enter your username or email",
|
||||
"password.validation.message": "Password criteria has not been met",
|
||||
"account.activation.success.message.title": "Success! You have activated your account.",
|
||||
@@ -113,8 +106,9 @@
|
||||
"recommendation.page.title": "Recommendations | {siteName}",
|
||||
"recommendation.page.heading": "We have a few recommendations to get you started.",
|
||||
"recommendation.skip.button": "Skip for now",
|
||||
"recommendation.option.trending": "Trending",
|
||||
"recommendation.option.trending": "Trending Now",
|
||||
"recommendation.option.popular": "Most Popular",
|
||||
"recommendation.option.recommended.for.you": "Recommended For You",
|
||||
"recommendation.product-card.pill-text.course": "Course",
|
||||
"recommendation.product-card.pill-text.professional-certificate": "Professional Certificate",
|
||||
"recommendation.product-card.pill-text.emeritus": "Offered on Emeritus",
|
||||
@@ -160,9 +154,6 @@
|
||||
"terms.of.service": "Terms of Service",
|
||||
"registration.username.suggestion.label": "Suggested:",
|
||||
"did.you.mean.alert.text": "Did you mean",
|
||||
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each\n Member process your personal data in accordance with the {privacyPolicy}.",
|
||||
"register.page.honor.code": "I agree to the {platformName} {tosAndHonorCode}",
|
||||
"register.page.terms.of.service": "I agree to the {platformName} {termsOfService}",
|
||||
"sign.in": "Sign in",
|
||||
"reset.password.page.title": "Reset Password | {siteName}",
|
||||
"reset.password": "Reset password",
|
||||
@@ -176,5 +167,15 @@
|
||||
"reset.server.rate.limit.error": "Too many requests.",
|
||||
"reset.password.success.heading": "Password reset complete.",
|
||||
"reset.password.success": "Your password has been reset. Sign in to your account.",
|
||||
"rate.limit.error": "An error has occurred because of too many requests. Please try again after some time."
|
||||
}
|
||||
"rate.limit.error": "An error has occurred because of too many requests. Please try again after some time.",
|
||||
"start.learning": "Start learning",
|
||||
"with.site.name": "with {siteName}",
|
||||
"your.career.turning.point": "Your career turning point",
|
||||
"is.here": "is here.",
|
||||
"welcome.to.platform": "Welcome to {siteName}, {username}!",
|
||||
"complete.your.profile.1": "Complete",
|
||||
"complete.your.profile.2": "your profile",
|
||||
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each Member process your personal data in accordance with the {privacyPolicy}.",
|
||||
"register.page.honor.code": "I agree to the {platformName} {tosAndHonorCode}",
|
||||
"register.page.terms.of.service": "I agree to the {platformName} {termsOfService}"
|
||||
}
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
{
|
||||
"start.learning": "Beginne zu lernen",
|
||||
"with.site.name": "mit {siteName}",
|
||||
"your.career.turning.point": "Your career turning point",
|
||||
"is.here": "is here.",
|
||||
"welcome.to.platform": "Willkommen bei {siteName}, {username}!",
|
||||
"complete.your.profile.1": "Vervollständige",
|
||||
"complete.your.profile.2": "dein Profil",
|
||||
"error.notfound.message": "Die gesuchte Seite ist nicht verfügbar oder es liegt ein Fehler in der URL vor. Bitte überprüfen Sie die URL und versuchen Sie es erneut.",
|
||||
"institution.login.page.sub.heading": "Wählen Sie Ihre Institution aus der folgenden Liste aus",
|
||||
"logistration.sign.in": "Anmelden",
|
||||
"logistration.register": "Registrieren",
|
||||
@@ -26,8 +20,7 @@
|
||||
"registration.using.tpa.form.heading": "Beenden Sie die Erstellung Ihres Kontos",
|
||||
"zendesk.supportTitle": "edX Support",
|
||||
"zendesk.selectTicketForm": "Please choose your request type:",
|
||||
"error.notfound.message": "Die gesuchte Seite ist nicht verfügbar oder es liegt ein Fehler in der URL vor. Bitte überprüfen Sie die URL und versuchen Sie es erneut.",
|
||||
"forgot.password.confirmation.message": "Wir haben eine E-Mail mit Anweisungen zum Zurücksetzen Ihres Passworts an {email} gesendet. Wenn Sie nach 1 Minute keine Nachricht zum Zurücksetzen des Passworts erhalten, überprüfen Sie, ob Sie die richtige E-Mail-Adresse eingegeben haben, oder überprüfen Sie Ihren Spam-Ordner. Wenn Sie weitere Hilfe benötigen, {supportLink}.",
|
||||
"forgot.password.confirmation.message": "We sent an email to {email} with instructions to reset your password. If you do not receive a password reset message after 1 minute, verify that you entered the correct email address, or check your spam folder. If you need further assistance, {supportLink}.",
|
||||
"forgot.password.page.title": "Passwort vergessen | {siteName}",
|
||||
"forgot.password.page.heading": "Passwort zurücksetzen",
|
||||
"forgot.password.page.instructions": "Bitte geben Sie unten Ihre E-Mail-Adresse ein und wir senden Ihnen eine E-Mail mit Anweisungen zum Zurücksetzen Ihres Passworts.",
|
||||
@@ -42,7 +35,7 @@
|
||||
"confirmation.message.title": "Prüfen Sie Ihr E-Mail-Postfach",
|
||||
"confirmation.support.link": "wenden Sie sich an den technischen Support",
|
||||
"need.help.sign.in.text": "Brauchen Sie Hilfe bei der Anmeldung?",
|
||||
"additional.help.text": "Wenden Sie sich für weitere Hilfe an den {platformName}-Support unter",
|
||||
"additional.help.text": "For additional help, contact {platformName} support at",
|
||||
"sign.in.text": "Anmelden",
|
||||
"extend.field.errors": "{emailError} unten.",
|
||||
"invalid.token.heading": "Ungültiger Link zum Zurücksetzen des Passworts",
|
||||
@@ -53,9 +46,9 @@
|
||||
"token.validation.internal.sever.error": "Ein Fehler ist aufgetreten. Versuchen Sie, die Seite zu aktualisieren, oder überprüfen Sie Ihre Internetverbindung.",
|
||||
"internal.server.error": "Ein Fehler ist aufgetreten. Versuchen Sie, die Seite zu aktualisieren, oder überprüfen Sie Ihre Internetverbindung.",
|
||||
"account.activation.error.message": "Etwas ist schief gelaufen, bitte {supportLink} um dieses Problem zu lösen.",
|
||||
"login.inactive.user.error": "Um sich anzumelden, müssen Sie Ihr Konto aktivieren.{lineBreak} {lineBreak}Wir haben gerade einen Aktivierungslink an {email} gesendet. Wenn Sie keine E-Mail erhalten, überprüfen Sie Ihre Spam-Ordner oder {supportLink}.",
|
||||
"login.inactive.user.error": "In order to sign in, you need to activate your account.{lineBreak} {lineBreak}We just sent an activation link to {email}. If you do not receive an email, check your spam folders or {supportLink}.",
|
||||
"allowed.domain.login.error": "Als {allowedDomain}-Benutzer müssen Sie sich mit Ihrem {allowedDomain} {tpaLink} anmelden.",
|
||||
"login.incorrect.credentials.error.attempts.text.1": "Der eingegebene Benutzername, die E-Mail oder das Passwort ist falsch. Sie haben {remainingAttempts} weitere Anmeldeversuche, bevor Ihr Konto vorübergehend gesperrt wird.",
|
||||
"login.incorrect.credentials.error.attempts.text.1": "The username, email or password you entered is incorrect. You have {remainingAttempts} more sign in attempts before your account is temporarily locked.",
|
||||
"login.incorrect.credentials.error.attempts.text.2": "Wenn Sie Ihr Passwort vergessen haben, {resetLink}",
|
||||
"account.locked.out.message.2": "Um auf der sicheren Seite zu sein, können Sie {resetLink} tun, bevor Sie es erneut versuchen.",
|
||||
"login.incorrect.credentials.error.with.reset.link": "Der eingegebene Benutzername, die E-Mail-Adresse oder das Passwort ist falsch. Bitte versuchen Sie es erneut oder {resetLink}.",
|
||||
@@ -71,7 +64,7 @@
|
||||
"non.compliant.password.message": "Ihr aktuelles Passwort entspricht nicht den neuen Sicherheitsanforderungen. Wir haben gerade eine Nachricht zum Zurücksetzen des Passworts an die mit diesem Konto verknüpfte E-Mail-Adresse gesendet. Vielen Dank, dass Sie uns helfen, Ihre Daten zu schützen.",
|
||||
"account.locked.out.message.1": "Um Ihr Konto zu schützen, wurde es vorübergehend gesperrt. Versuchen Sie es in 30 Minuten erneut.",
|
||||
"enterprise.login.btn.text": "Arbeits- oder Schulzeugnisse",
|
||||
"username.or.email.format.validation.less.chars.message": "Benutzername oder E-Mail müssen mindestens 3 Zeichen lang sein.",
|
||||
"username.or.email.format.validation.less.chars.message": "Benutzername oder E-Mail müssen mindestens 2 Zeichen lang sein.",
|
||||
"email.validation.message": "Geben Sie Ihren Benutzernamen oder Ihre E-Mail-Adresse ein",
|
||||
"password.validation.message": "Die Passwortkriterien wurden nicht erfüllt",
|
||||
"account.activation.success.message.title": "Super! Sie haben Ihr Konto aktiviert.",
|
||||
@@ -113,8 +106,9 @@
|
||||
"recommendation.page.title": "Empfehlungen | {siteName}",
|
||||
"recommendation.page.heading": "Wir haben ein paar Empfehlungen für den Einstieg.",
|
||||
"recommendation.skip.button": "Überspringen",
|
||||
"recommendation.option.trending": "Trending",
|
||||
"recommendation.option.trending": "Trending Now",
|
||||
"recommendation.option.popular": "Most Popular",
|
||||
"recommendation.option.recommended.for.you": "Recommended For You",
|
||||
"recommendation.product-card.pill-text.course": "Course",
|
||||
"recommendation.product-card.pill-text.professional-certificate": "Professional Certificate",
|
||||
"recommendation.product-card.pill-text.emeritus": "Offered on Emeritus",
|
||||
@@ -160,9 +154,6 @@
|
||||
"terms.of.service": "Nutzungsbedingungen",
|
||||
"registration.username.suggestion.label": "Empfohlen:",
|
||||
"did.you.mean.alert.text": "Meinten Sie",
|
||||
"register.page.terms.of.service.and.honor.code": "Wenn Sie ein Konto erstellen, stimmen Sie den {tosAndHonorCode} zu und erkennen an, dass {platformName} und jedes \nMitglied Ihre personenbezogenen Daten in Übereinstimmung mit den {privacyPolicy} verarbeitet.",
|
||||
"register.page.honor.code": "Ich stimme den {platformName} {tosAndHonorCode} zu",
|
||||
"register.page.terms.of.service": "Ich stimme den {platformName} {termsOfService} zu",
|
||||
"sign.in": "Anmelden",
|
||||
"reset.password.page.title": "Passwort zurücksetzen | {siteName}",
|
||||
"reset.password": "Passwort zurücksetzen",
|
||||
@@ -176,5 +167,15 @@
|
||||
"reset.server.rate.limit.error": "Zu viele Anfragen.",
|
||||
"reset.password.success.heading": "Zurücksetzen des Passworts abgeschlossen.",
|
||||
"reset.password.success": "Ihr Passwort wurde zurückgesetzt. Melden Sie sich bei Ihrem Konto an.",
|
||||
"rate.limit.error": "Aufgrund zu vieler Anfragen ist ein Fehler aufgetreten. Bitte versuchen Sie es nach einiger Zeit erneut."
|
||||
}
|
||||
"rate.limit.error": "Aufgrund zu vieler Anfragen ist ein Fehler aufgetreten. Bitte versuchen Sie es nach einiger Zeit erneut.",
|
||||
"start.learning": "Beginne zu lernen",
|
||||
"with.site.name": "mit {siteName}",
|
||||
"your.career.turning.point": "Your career turning point",
|
||||
"is.here": "is here.",
|
||||
"welcome.to.platform": "Willkommen bei {siteName}, {username}!",
|
||||
"complete.your.profile.1": "Vervollständige",
|
||||
"complete.your.profile.2": "dein Profil",
|
||||
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each Member process your personal data in accordance with the {privacyPolicy}.",
|
||||
"register.page.honor.code": "I agree to the {platformName} {tosAndHonorCode}",
|
||||
"register.page.terms.of.service": "I agree to the {platformName} {termsOfService}"
|
||||
}
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
{
|
||||
"start.learning": "Empieza a aprender",
|
||||
"with.site.name": "con {siteName}",
|
||||
"your.career.turning.point": "El punto de inflexión de tu carrera",
|
||||
"is.here": "es aquí.",
|
||||
"welcome.to.platform": "¡Bienvenido a {siteName}, {username}!",
|
||||
"complete.your.profile.1": "Completado",
|
||||
"complete.your.profile.2": "tu perfil ",
|
||||
"error.notfound.message": "La página que estas buscando no está disponible o hay un error en la URL. Por favor, verifica la URL y vuelve a intentarlo.",
|
||||
"institution.login.page.sub.heading": "Selecciona tu institución de la lista siguiente",
|
||||
"logistration.sign.in": "Iniciar sesión",
|
||||
"logistration.register": "Registrarse",
|
||||
@@ -26,8 +20,7 @@
|
||||
"registration.using.tpa.form.heading": "Termina de crear tu cuenta",
|
||||
"zendesk.supportTitle": "Soporte edX",
|
||||
"zendesk.selectTicketForm": "Elegir el tipo de solicitud:",
|
||||
"error.notfound.message": "La página que estas buscando no está disponible o hay un error en la URL. Por favor, verifica la URL y vuelve a intentarlo.",
|
||||
"forgot.password.confirmation.message": "Hemos enviado un correo electrónico a {email} con instrucciones para restablecer tu contraseña.\n Si no recibes un mensaje de restablecimiento de contraseña después de 1 minuto, verifica que has introducido\n la dirección de correo electrónico correcta, o comprueba tu carpeta de correo no deseado. Si necesitas más ayuda, {supportLink}.",
|
||||
"forgot.password.confirmation.message": "Enviamos un correo electrónico a {email} con instrucciones para restablecer su contraseña. Si no recibe un mensaje de restablecimiento de contraseña después de 1 minuto, verifique que ingresó la dirección de correo electrónico correcta o verifique su carpeta de correo no deseado. Si necesita más ayuda, {supportLink}.",
|
||||
"forgot.password.page.title": "Olvidé la contraseña | {siteName}",
|
||||
"forgot.password.page.heading": "Restablecer mi contraseña",
|
||||
"forgot.password.page.instructions": "Por favor, introduce tu dirección de correo electrónico y te enviaremos un correo electrónico con instrucciones sobre cómo restablecer tu contraseña.",
|
||||
@@ -42,7 +35,7 @@
|
||||
"confirmation.message.title": "Verifica tu correo electrónico",
|
||||
"confirmation.support.link": "entra en contacto con el equipo de soporte técnico",
|
||||
"need.help.sign.in.text": "¿Necesitas ayuda para iniciar sesión?",
|
||||
"additional.help.text": "Para obtener ayuda adicional, comuníquese con el soporte de {platformName} en",
|
||||
"additional.help.text": "Para obtener ayuda adicional, comuníquese con el soporte {platformName} en",
|
||||
"sign.in.text": "Iniciar sesión",
|
||||
"extend.field.errors": "{emailError} a continuación.",
|
||||
"invalid.token.heading": "Enlace de restablecimiento de contraseña inválido",
|
||||
@@ -53,9 +46,9 @@
|
||||
"token.validation.internal.sever.error": "Se ha producido un error. Intenta actualizar la página o verifica tu conexión a Internet.",
|
||||
"internal.server.error": "Se ha producido un error. Intenta actualizar la página o verifica tu conexión a Internet.",
|
||||
"account.activation.error.message": "Algo no funcionó correctamente, por favor {supportLink} para resolver este problema.",
|
||||
"login.inactive.user.error": "Para iniciar sesión, debes activar tu cuenta..{lineBreak}\n {lineBreak} Acabamos de enviar un enlace de activación a {email}. Si no recibes un correo electrónico,\n revisa tus carpetas de spam o {supportLink}.",
|
||||
"login.inactive.user.error": "Para iniciar sesión, debe activar su cuenta.{lineBreak} {lineBreak}Acabamos de enviar un enlace de activación a {email}. Si no recibe un correo electrónico, revise sus carpetas de spam o {supportLink}.",
|
||||
"allowed.domain.login.error": "Como usuario {allowedDomain}, debe iniciar sesión con su {allowedDomain} {tpaLink}.",
|
||||
"login.incorrect.credentials.error.attempts.text.1": "El nombre de usuario, el email o la contraseña que has introducido son incorrectos. Tienes {remainingAttempts} intentos más de inicio de sesión\n antes de que tu cuenta se bloquee temporalmente.",
|
||||
"login.incorrect.credentials.error.attempts.text.1": "El nombre de usuario, correo electrónico o contraseña que ingresó es incorrecto. Tiene {remainingAttempts} más intentos de inicio de sesión antes de que su cuenta se bloquee temporalmente.",
|
||||
"login.incorrect.credentials.error.attempts.text.2": "Si has olvidado tu contraseña, {resetLink}",
|
||||
"account.locked.out.message.2": "Para estar seguro, puedes {resetLink} antes de volver a intentarlo.",
|
||||
"login.incorrect.credentials.error.with.reset.link": "El nombre de usuario, el correo electrónico o la contraseña que has introducido son incorrectos. Por favor, inténtalo de nuevo o {resetLink}.",
|
||||
@@ -71,7 +64,7 @@
|
||||
"non.compliant.password.message": "Tu contraseña actual no cumple con los nuevos requisitos de seguridad. Acabamos de enviar un mensaje de restablecimiento de contraseña a la dirección de correo electrónico asociada a esta cuenta. Gracias por ayudarnos a mantener tus datos seguros.",
|
||||
"account.locked.out.message.1": "Para proteger tu cuenta, se ha bloqueado temporalmente. Inténtalo de nuevo en 30 minutos.",
|
||||
"enterprise.login.btn.text": "Credenciales de la empresa o de la institución ",
|
||||
"username.or.email.format.validation.less.chars.message": "El nombre de usuario o el correo electrónico deben tener al menos 3 caracteres.",
|
||||
"username.or.email.format.validation.less.chars.message": "El nombre de usuario o el correo electrónico deben tener al menos 2 caracteres.",
|
||||
"email.validation.message": "Introduce tu nombre de usuario o correo electrónico",
|
||||
"password.validation.message": "No se han cumplido los criterios de la contraseña",
|
||||
"account.activation.success.message.title": "Ha sido un éxito. Has activado tu cuenta.",
|
||||
@@ -115,6 +108,7 @@
|
||||
"recommendation.skip.button": "Saltar por ahora ",
|
||||
"recommendation.option.trending": "Tendencias",
|
||||
"recommendation.option.popular": "Más popular",
|
||||
"recommendation.option.recommended.for.you": "Recomendado para usted",
|
||||
"recommendation.product-card.pill-text.course": "Curso",
|
||||
"recommendation.product-card.pill-text.professional-certificate": "Certificado profesional",
|
||||
"recommendation.product-card.pill-text.emeritus": "Ofrecido en Emeritus",
|
||||
@@ -160,9 +154,6 @@
|
||||
"terms.of.service": "Términos de servicio",
|
||||
"registration.username.suggestion.label": "Se recomienda:",
|
||||
"did.you.mean.alert.text": "¿Quieres decir",
|
||||
"register.page.terms.of.service.and.honor.code": "Al crear una cuenta, aceptas el {tosAndHonorCode} y reconoces que {platformName} y cada\n Miembro procesa tus datos personales de acuerdo con la {privacyPolicy}.",
|
||||
"register.page.honor.code": "Acepto las {platformName} {tosAndHonorCode}",
|
||||
"register.page.terms.of.service": "Acepto las {platformName} {termsOfService}",
|
||||
"sign.in": "Iniciar sesión",
|
||||
"reset.password.page.title": "Restablecer contraseña | {siteName}",
|
||||
"reset.password": "Restablecer mi contraseña",
|
||||
@@ -176,5 +167,15 @@
|
||||
"reset.server.rate.limit.error": "Demasiadas solicitudes.",
|
||||
"reset.password.success.heading": "Restablecimiento de la contraseña completado.",
|
||||
"reset.password.success": "Tu contraseña ha sido restablecida. Acceda a tu cuenta.",
|
||||
"rate.limit.error": "Se ha producido un error debido a demasiadas solicitudes. Por favor, inténtalo de nuevo después de algún tiempo."
|
||||
}
|
||||
"rate.limit.error": "Se ha producido un error debido a demasiadas solicitudes. Por favor, inténtalo de nuevo después de algún tiempo.",
|
||||
"start.learning": "Empieza a aprender",
|
||||
"with.site.name": "con {siteName}",
|
||||
"your.career.turning.point": "El punto de inflexión de tu carrera",
|
||||
"is.here": "es aquí.",
|
||||
"welcome.to.platform": "¡Bienvenido a {siteName}, {username}!",
|
||||
"complete.your.profile.1": "Completado",
|
||||
"complete.your.profile.2": "tu perfil ",
|
||||
"register.page.terms.of.service.and.honor.code": "Al crear una cuenta, acepta {tosAndHonorCode} y reconoce que {platformName} y cada miembro procesan sus datos personales de acuerdo con {privacyPolicy}.",
|
||||
"register.page.honor.code": "Acepto las {platformName} {tosAndHonorCode}",
|
||||
"register.page.terms.of.service": "Acepto las {platformName} {termsOfService}"
|
||||
}
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
{
|
||||
"start.learning": "آغاز یادگیری",
|
||||
"with.site.name": "با {siteName}",
|
||||
"your.career.turning.point": "نقطه عطف حرفه ای شما",
|
||||
"is.here": "اینجاست.",
|
||||
"welcome.to.platform": "خوش آمدید به {siteName}, {username}!",
|
||||
"complete.your.profile.1": "کامل",
|
||||
"complete.your.profile.2": "پرونده کاربری شما",
|
||||
"error.notfound.message": "صفحه مورد نظر شما در دسترس نیست یا خطایی در نشانی اینترنتی وجود دارد. لطفاً نشانی اینترنتی را بررسی کرده و دوباره تلاش کنید.",
|
||||
"institution.login.page.sub.heading": "موسسه خود را از فهرست زیر برگزینید",
|
||||
"logistration.sign.in": "ورود",
|
||||
"logistration.register": "ثبتنام",
|
||||
@@ -26,8 +20,7 @@
|
||||
"registration.using.tpa.form.heading": "ساخت حساب کاربری خود را به اتمام برسانید",
|
||||
"zendesk.supportTitle": "پشتیبانی edX",
|
||||
"zendesk.selectTicketForm": "لطفا نوع درخواست خود را انتخاب کنید:",
|
||||
"error.notfound.message": "صفحه مورد نظر شما در دسترس نیست یا خطایی در نشانی اینترنتی وجود دارد. لطفاً نشانی اینترنتی را بررسی کرده و دوباره تلاش کنید.",
|
||||
"forgot.password.confirmation.message": "ما رایانامهای بههمراه دستورالعمل بازنشانی گذرواژه به {email} ارسال کردیم. اگر پس از 1 دقیقه پیام بازنشانی را دریافت نکردید، بررسی کنید که نشانی رایانامه را صحیح وارد کردهاید یا پوشه هرزنامه خود را بررسی کنید. اگر به دریافت راهنمایی بیشتری، با {supportLink} تماس بگیرید.",
|
||||
"forgot.password.confirmation.message": "We sent an email to {email} with instructions to reset your password. If you do not receive a password reset message after 1 minute, verify that you entered the correct email address, or check your spam folder. If you need further assistance, {supportLink}.",
|
||||
"forgot.password.page.title": "فراموش گذرواژه | {siteName}",
|
||||
"forgot.password.page.heading": "بازتنظیم گذرواژه",
|
||||
"forgot.password.page.instructions": "لطفا نشانی رایانامه خود را در قسمت زیر وارد کنید و ما رایانامهای حاوی دستورالعمل نحوه بازتنظیم مجدد گذرواژه برای شما ارسال خواهیم کرد.",
|
||||
@@ -42,7 +35,7 @@
|
||||
"confirmation.message.title": "صندوق رایانامه خود را ببینید",
|
||||
"confirmation.support.link": "با پشتیبانی فنی تماس بگیرید",
|
||||
"need.help.sign.in.text": "برای ورود به سامانه نیاز به کمک دارید؟",
|
||||
"additional.help.text": "برای دریافت راهنمایی بیشتر، با پشتیبانی {platformName} در این آدرس تماس بگیرید",
|
||||
"additional.help.text": "For additional help, contact {platformName} support at",
|
||||
"sign.in.text": "ورود",
|
||||
"extend.field.errors": "{emailError} زیر.",
|
||||
"invalid.token.heading": "پیوند بازتنظیم گذرواژه معتبر نیست",
|
||||
@@ -53,9 +46,9 @@
|
||||
"token.validation.internal.sever.error": "خطایی رخ داده است. صفحه را دوباره بارگیری کنید یا اتصال اینترنت خود را بررسی کنید.",
|
||||
"internal.server.error": "خطایی رخ داده است. صفحه را دوباره بارگیری کنید یا اتصال اینترنت خود را بررسی کنید.",
|
||||
"account.activation.error.message": "اشتباهی رخ داد، لطفاً برای حل این مساله ، به این قسمت{supportLink} مراجعه کنید.",
|
||||
"login.inactive.user.error": "برای ورود به سامانه، باید حساب کاربری خود را فعال کنید.{lineBreak} {lineBreak}هم اکنون پیوند فعالسازی را به {email} فرستادیم. اگر رایانامهای دریافت نکردید، پوشههای هرزنامه یا {supportLink} را بررسی کنید.",
|
||||
"login.inactive.user.error": "In order to sign in, you need to activate your account.{lineBreak} {lineBreak}We just sent an activation link to {email}. If you do not receive an email, check your spam folders or {supportLink}.",
|
||||
"allowed.domain.login.error": "به عنوان کاربر {allowedDomain}، باید با {allowedDomain} {tpaLink} خود وارد شوید.",
|
||||
"login.incorrect.credentials.error.attempts.text.1": "نام کاربری، نشانی رایانامه یا گذرواژهای که وارد کردهاید نادرست است. پیش از اینکه حساب کاربری شما موقتا قفل شود، {remainingAttempts} تلاش دیگر برای ورود به سامانه دارید.",
|
||||
"login.incorrect.credentials.error.attempts.text.1": "The username, email or password you entered is incorrect. You have {remainingAttempts} more sign in attempts before your account is temporarily locked.",
|
||||
"login.incorrect.credentials.error.attempts.text.2": "اگر گذرواژه خود را فراموش کردهاید، {resetLink}",
|
||||
"account.locked.out.message.2": "برای حفظ امنیت، میتوانید پیش از تلاش مجدد، {resetLink} را انجام دهید.",
|
||||
"login.incorrect.credentials.error.with.reset.link": "نام کاربری، نشانی رایانامه یا گذرواژهای که وارد کردید درست نیست. لطفاً دوباره امتحان کنید یا {resetLink}.",
|
||||
@@ -71,7 +64,7 @@
|
||||
"non.compliant.password.message": "گذرواژه فعلی شما الزامات امنیتی جدید را برآورده نمیکند. ما فقط یک پیام بازتنظیم گذرواژه به نشانی رایانامه مرتبط با این حساب کاربری ارسال کردیم. از اینکه به ما کمک میکنید تا دادههای شما را ایمن نگه دارید متشکریم.",
|
||||
"account.locked.out.message.1": "حساب کاربری شما، به دلیل حفاظت، بهطور موقت قفل شده است. 30 دقیقه دیگر دوباره امتحان کنید.",
|
||||
"enterprise.login.btn.text": "اعتبار دانشکده یا شرکت",
|
||||
"username.or.email.format.validation.less.chars.message": "نام کاربری یا نشانی رایانامه حداقل باید 3 نویسه داشته باشد",
|
||||
"username.or.email.format.validation.less.chars.message": "نام کاربری یا نشانی رایانامه حداقل باید 2 نویسه داشته باشد",
|
||||
"email.validation.message": "نام کاربری یا رایانامه خود را وارد کنید",
|
||||
"password.validation.message": "معیارهای گذرواژه رعایت نشده است",
|
||||
"account.activation.success.message.title": "موفق شدید! شما حساب کاربری خود را فعال کردید.",
|
||||
@@ -113,8 +106,9 @@
|
||||
"recommendation.page.title": "توصیه ها | {siteName}",
|
||||
"recommendation.page.heading": "ما چند توصیه برای شروع کار داریم.",
|
||||
"recommendation.skip.button": "فعلا بگذرید",
|
||||
"recommendation.option.trending": "پرطرفدار",
|
||||
"recommendation.option.trending": "Trending Now",
|
||||
"recommendation.option.popular": "محبوبترین",
|
||||
"recommendation.option.recommended.for.you": "Recommended For You",
|
||||
"recommendation.product-card.pill-text.course": "دوره آموزشی",
|
||||
"recommendation.product-card.pill-text.professional-certificate": "گواهی حرفهای",
|
||||
"recommendation.product-card.pill-text.emeritus": "ارائه شده در Emeritus",
|
||||
@@ -160,9 +154,6 @@
|
||||
"terms.of.service": "شرایط استفاده از خدمات",
|
||||
"registration.username.suggestion.label": "پیشنهادشده:",
|
||||
"did.you.mean.alert.text": "منظور شما این بود",
|
||||
"register.page.terms.of.service.and.honor.code": "با ایجاد یک حساب کاربری، با {tosAndHonorCode} موافقت میکنید و تصدیق میکنید که {platformName} و هر عضو دادههای شخصی شما را مطابق با {privacyPolicy} پردازش میکنند.",
|
||||
"register.page.honor.code": "من با {platformName} {tosAndHonorCode} موافقم",
|
||||
"register.page.terms.of.service": "من با {platformName} {termsOfService} موافقم",
|
||||
"sign.in": "ورود",
|
||||
"reset.password.page.title": "بازتنظیم گذرواژه | {siteName}",
|
||||
"reset.password": "بازتنظیم گذرواژه",
|
||||
@@ -176,5 +167,15 @@
|
||||
"reset.server.rate.limit.error": "تعداد درخواستها خیلی زیاد است.",
|
||||
"reset.password.success.heading": "بازتنظیم گذرواژه تکمیل شد.",
|
||||
"reset.password.success": "گذرواژه شما بازتنظیم شد. وارد حساب کاربری خود شوید",
|
||||
"rate.limit.error": "به دلیل درخواستهای زیاد، خطایی روی داده است. لطفا بعد از مدتی دوباره امتحان کنید."
|
||||
}
|
||||
"rate.limit.error": "به دلیل درخواستهای زیاد، خطایی روی داده است. لطفا بعد از مدتی دوباره امتحان کنید.",
|
||||
"start.learning": "آغاز یادگیری",
|
||||
"with.site.name": "با {siteName}",
|
||||
"your.career.turning.point": "نقطه عطف حرفه ای شما",
|
||||
"is.here": "اینجاست.",
|
||||
"welcome.to.platform": "خوش آمدید به {siteName}, {username}!",
|
||||
"complete.your.profile.1": "کامل",
|
||||
"complete.your.profile.2": "پرونده کاربری شما",
|
||||
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each Member process your personal data in accordance with the {privacyPolicy}.",
|
||||
"register.page.honor.code": "I agree to the {platformName} {tosAndHonorCode}",
|
||||
"register.page.terms.of.service": "I agree to the {platformName} {termsOfService}"
|
||||
}
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
{
|
||||
"start.learning": "Démarrer l'apprentissage",
|
||||
"with.site.name": "avec {siteName}",
|
||||
"your.career.turning.point": "Your career turning point",
|
||||
"is.here": "is here.",
|
||||
"welcome.to.platform": "Bienvenue sur {siteName}, {username}!",
|
||||
"complete.your.profile.1": "Terminé",
|
||||
"complete.your.profile.2": "votre profil",
|
||||
"error.notfound.message": "La page que vous recherchez n'est pas disponible ou il y a une erreur dans l'URL. Veuillez vérifier l'URL et réessayer.",
|
||||
"institution.login.page.sub.heading": "Sélectionner votre institution dans la liste ci-dessous",
|
||||
"logistration.sign.in": "Connectez-vous",
|
||||
"logistration.register": "S'inscrire",
|
||||
@@ -26,8 +20,7 @@
|
||||
"registration.using.tpa.form.heading": "Terminer la création de votre compte",
|
||||
"zendesk.supportTitle": "edX Support",
|
||||
"zendesk.selectTicketForm": "Please choose your request type:",
|
||||
"error.notfound.message": "La page que vous recherchez n'est pas disponible ou il y a une erreur dans l'URL. Veuillez vérifier l'URL et réessayer.",
|
||||
"forgot.password.confirmation.message": "Nous avons envoyé un courriel à {email} avec des instructions pour réinitialiser votre mot de passe.\n Si vous ne recevez pas de message de réinitialisation de mot de passe après 1 minute, vérifiez que vous avez saisi\nl'adresse courriel correctement, ou vérifiez votre dossier de courriel indésirable. Si vous avez besoin d'aide supplémentaire, {supportLink}.",
|
||||
"forgot.password.confirmation.message": "We sent an email to {email} with instructions to reset your password. If you do not receive a password reset message after 1 minute, verify that you entered the correct email address, or check your spam folder. If you need further assistance, {supportLink}.",
|
||||
"forgot.password.page.title": " Mot de passe oublié | {siteName}",
|
||||
"forgot.password.page.heading": "Réinitialiser le mot de passe",
|
||||
"forgot.password.page.instructions": "Veuillez entrer votre adresse courriel ci-dessous et nous vous enverrons un courriel avec les instructions pour réinitialiser votre mot de passe.",
|
||||
@@ -42,7 +35,7 @@
|
||||
"confirmation.message.title": "Vérifiez votre email",
|
||||
"confirmation.support.link": "contacter le support technique",
|
||||
"need.help.sign.in.text": "Besoin d'aide pour vous enregistrer?",
|
||||
"additional.help.text": "For additional help, contact {platformName} support at ",
|
||||
"additional.help.text": "For additional help, contact {platformName} support at",
|
||||
"sign.in.text": "Connectez-vous",
|
||||
"extend.field.errors": "{emailError} ci-dessous.",
|
||||
"invalid.token.heading": "Lien de réinitialisation du mot de passe non valide",
|
||||
@@ -53,9 +46,9 @@
|
||||
"token.validation.internal.sever.error": "Une erreur est survenue. Essayer de rafraîchir la page, ou vérifier votre connexion Internet.",
|
||||
"internal.server.error": "Une erreur est survenue. Essayer de rafraîchir la page, ou vérifier votre connexion Internet.",
|
||||
"account.activation.error.message": "Une erreur s'est produite, veuillez {supportLink} pour résoudre ce problème.",
|
||||
"login.inactive.user.error": "Pour vous connecter, vous devez activer votre compte.{lineBreak}\n {lineBreak}Nous venons d'envoyer un lien d'activation à {email}. Si vous ne recevez pas de courriel,\n vérifiez vos dossiers de spam ou {supportLink}.",
|
||||
"login.inactive.user.error": "In order to sign in, you need to activate your account.{lineBreak} {lineBreak}We just sent an activation link to {email}. If you do not receive an email, check your spam folders or {supportLink}.",
|
||||
"allowed.domain.login.error": "As {allowedDomain} user, You must login with your {allowedDomain} {tpaLink}.",
|
||||
"login.incorrect.credentials.error.attempts.text.1": "Le nom d'utilisateur, le courriel ou le mot de passe que vous avez entré est incorrect. Vous avez {remainingAttempts} tentatives\n de connexion avant que votre compte soit temporairement verrouillé.",
|
||||
"login.incorrect.credentials.error.attempts.text.1": "The username, email or password you entered is incorrect. You have {remainingAttempts} more sign in attempts before your account is temporarily locked.",
|
||||
"login.incorrect.credentials.error.attempts.text.2": "Si vous avez oublié votre mot de passe, {resetLink}",
|
||||
"account.locked.out.message.2": "Par mesure de sécurité, vous pouvez {resetLink} avant de réessayer.",
|
||||
"login.incorrect.credentials.error.with.reset.link": "Le nom d'utilisateur, l'adresse courriel ou le mot de passe que vous avez saisis sont incorrects. Veuillez réessayer ou {resetLink}.",
|
||||
@@ -71,7 +64,7 @@
|
||||
"non.compliant.password.message": "Votre mot de passe actuel ne répond pas aux nouvelles exigences de sécurité. Nous venons d'envoyer un message de réinitialisation de mot de passe à l'adresse courriel associée à ce compte. Merci de nous aider à protéger vos données.",
|
||||
"account.locked.out.message.1": "Pour protéger votre compte, il a été temporairement verrouillé. Réessayez dans 30 minutes.",
|
||||
"enterprise.login.btn.text": "Identifiants de la compagnie ou de l'école",
|
||||
"username.or.email.format.validation.less.chars.message": "Le nom d'utilisateur ou l'adresse courriel doit comporter au moins 3 caractères.",
|
||||
"username.or.email.format.validation.less.chars.message": "Le nom d'utilisateur ou l'adresse courriel doit comporter au moins 2 caractères.",
|
||||
"email.validation.message": "Entrez votre nom d'utilisateur ou votre adresse courriel",
|
||||
"password.validation.message": "Les critères de mot de passe n'ont pas été remplis",
|
||||
"account.activation.success.message.title": "Succès! Vous avez activé votre compte.",
|
||||
@@ -113,8 +106,9 @@
|
||||
"recommendation.page.title": "Recommendations | {siteName}",
|
||||
"recommendation.page.heading": "We have a few recommendations to get you started.",
|
||||
"recommendation.skip.button": "Skip for now",
|
||||
"recommendation.option.trending": "Trending",
|
||||
"recommendation.option.trending": "Trending Now",
|
||||
"recommendation.option.popular": "Most Popular",
|
||||
"recommendation.option.recommended.for.you": "Recommended For You",
|
||||
"recommendation.product-card.pill-text.course": "Course",
|
||||
"recommendation.product-card.pill-text.professional-certificate": "Professional Certificate",
|
||||
"recommendation.product-card.pill-text.emeritus": "Offered on Emeritus",
|
||||
@@ -160,9 +154,6 @@
|
||||
"terms.of.service": " Conditions d'utilisation",
|
||||
"registration.username.suggestion.label": "Suggéré :",
|
||||
"did.you.mean.alert.text": "Vouliez-vous dire",
|
||||
"register.page.terms.of.service.and.honor.code": "En créant un compte, vous acceptez le {tosAndHonorCode} et vous reconnaissez que {platformName} et chaque\n membre peut traiter vos données personnelles conformément à la {privacyPolicy}.",
|
||||
"register.page.honor.code": "I agree to the {platformName} {tosAndHonorCode}",
|
||||
"register.page.terms.of.service": "I agree to the {platformName} {termsOfService}",
|
||||
"sign.in": "Connectez-vous",
|
||||
"reset.password.page.title": "Réinitialiser le mot de passe | {siteName}",
|
||||
"reset.password": "Réinitialiser le mot de passe",
|
||||
@@ -176,5 +167,15 @@
|
||||
"reset.server.rate.limit.error": "Trop de demandes.",
|
||||
"reset.password.success.heading": "Réinitialisation du mot de passe complétée.",
|
||||
"reset.password.success": "Votre mot de passe a été réinitialisé. Connectez-vous à votre compte.",
|
||||
"rate.limit.error": "Une erreur s'est produite en raison d'un trop grand nombre de demandes. Veuillez réessayer après un certain temps."
|
||||
}
|
||||
"rate.limit.error": "Une erreur s'est produite en raison d'un trop grand nombre de demandes. Veuillez réessayer après un certain temps.",
|
||||
"start.learning": "Démarrer l'apprentissage",
|
||||
"with.site.name": "avec {siteName}",
|
||||
"your.career.turning.point": "Your career turning point",
|
||||
"is.here": "is here.",
|
||||
"welcome.to.platform": "Bienvenue sur {siteName}, {username}!",
|
||||
"complete.your.profile.1": "Terminé",
|
||||
"complete.your.profile.2": "votre profil",
|
||||
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each Member process your personal data in accordance with the {privacyPolicy}.",
|
||||
"register.page.honor.code": "I agree to the {platformName} {tosAndHonorCode}",
|
||||
"register.page.terms.of.service": "I agree to the {platformName} {termsOfService}"
|
||||
}
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
{
|
||||
"start.learning": "Commencez à apprendre",
|
||||
"with.site.name": "avec {siteName}",
|
||||
"your.career.turning.point": "Votre tournant de carrière",
|
||||
"is.here": "est là.",
|
||||
"welcome.to.platform": "Bienvenue sur {siteName}, {username}!",
|
||||
"complete.your.profile.1": "Complet",
|
||||
"complete.your.profile.2": "votre profil",
|
||||
"error.notfound.message": "La page que vous recherchez n'est pas disponible ou il y a une erreur dans l'URL. Veuillez vérifier l'URL et réessayer.",
|
||||
"institution.login.page.sub.heading": "Sélectionner votre institution dans la liste ci-dessous",
|
||||
"logistration.sign.in": "Connexion",
|
||||
"logistration.register": "Inscription",
|
||||
@@ -26,8 +20,7 @@
|
||||
"registration.using.tpa.form.heading": "Terminer la création de votre compte",
|
||||
"zendesk.supportTitle": "Prise en charge d'edX",
|
||||
"zendesk.selectTicketForm": "Veuillez choisir votre type de demande :",
|
||||
"error.notfound.message": "La page que vous recherchez n'est pas disponible ou il y a une erreur dans l'URL. Veuillez vérifier l'URL et réessayer.",
|
||||
"forgot.password.confirmation.message": "Nous avons envoyé un courriel à {email} avec des instructions pour réinitialiser votre mot de passe.\n Si vous ne recevez pas de message de réinitialisation de mot de passe après 1 minute, vérifiez que vous avez saisi\nl'adresse courriel correctement, ou vérifiez votre dossier de pourriels. Si vous avez besoin d'aide supplémentaire, {supportLink}.",
|
||||
"forgot.password.confirmation.message": "Nous avons envoyé un courriel à {email} avec des instructions pour réinitialiser votre mot de passe. Si vous ne recevez pas de message de réinitialisation de mot de passe après 1 minute, vérifiez que vous avez saisi l'adresse courriel correctement, ou vérifiez votre dossier de pourriels. Si vous avez besoin d'aide supplémentaire, contactez {supportLink}.",
|
||||
"forgot.password.page.title": "Mot de passe oublié | {siteName}",
|
||||
"forgot.password.page.heading": "Réinitialiser le mot de passe",
|
||||
"forgot.password.page.instructions": "Veuillez entrer votre adresse courriel ci-dessous et nous vous enverrons un courriel avec les instructions pour réinitialiser votre mot de passe.",
|
||||
@@ -42,7 +35,7 @@
|
||||
"confirmation.message.title": "Vérifiez votre courriel",
|
||||
"confirmation.support.link": "contacter le support technique",
|
||||
"need.help.sign.in.text": "Besoin d'aide pour vous connecter?",
|
||||
"additional.help.text": "Pour obtenir une aide supplémentaire, contactez l'assistance {platformName} à l'adresse ",
|
||||
"additional.help.text": "Pour obtenir de l'aide supplémentaire, contactez l'assistance {platformName} à l'adresse",
|
||||
"sign.in.text": "Connexion",
|
||||
"extend.field.errors": "{emailError} ci-dessous.",
|
||||
"invalid.token.heading": "Lien de réinitialisation du mot de passe non valide",
|
||||
@@ -53,9 +46,9 @@
|
||||
"token.validation.internal.sever.error": "Une erreur est survenue. Essayer de rafraîchir la page, ou vérifier votre connexion Internet.",
|
||||
"internal.server.error": "Une erreur est survenue. Essayer de rafraîchir la page, ou vérifier votre connexion Internet.",
|
||||
"account.activation.error.message": "Une erreur s'est produite, veuillez {supportLink} pour résoudre ce problème.",
|
||||
"login.inactive.user.error": "Pour vous connecter, vous devez activer votre compte.{lineBreak}\n {lineBreak}Nous venons d'envoyer un lien d'activation à {email}. Si vous ne recevez pas de courriel,\n vérifiez vos dossiers de pourriels ou {supportLink}.",
|
||||
"login.inactive.user.error": "Pour vous connecter, vous devez activer votre compte.{lineBreak}{lineBreak}Nous venons d'envoyer un lien d'activation à {email}. Si vous ne recevez pas de courriel, vérifiez vos dossiers de pourriels ou {supportLink}.",
|
||||
"allowed.domain.login.error": "En tant qu'utilisateur {allowedDomain}, vous devez vous connecter avec votre {allowedDomain} {tpaLink}.",
|
||||
"login.incorrect.credentials.error.attempts.text.1": "Le nom d'utilisateur, le courriel ou le mot de passe que vous avez entré sont incorrects. Vous avez {remainingAttempts} tentatives\n de connexion avant que votre compte soit temporairement verrouillé.",
|
||||
"login.incorrect.credentials.error.attempts.text.1": "Le nom d'utilisateur, le courriel ou le mot de passe que vous avez entré sont incorrects. Vous avez {remainingAttempts} tentatives de connexion avant que votre compte soit temporairement verrouillé.",
|
||||
"login.incorrect.credentials.error.attempts.text.2": "Si vous avez oublié votre mot de passe, {resetLink}",
|
||||
"account.locked.out.message.2": "Par mesure de sécurité, vous pouvez {resetLink} avant de réessayer.",
|
||||
"login.incorrect.credentials.error.with.reset.link": "Le nom d'utilisateur, l'adresse courriel ou le mot de passe que vous avez saisis sont incorrects. Veuillez réessayer ou {resetLink}.",
|
||||
@@ -71,7 +64,7 @@
|
||||
"non.compliant.password.message": "Votre mot de passe actuel ne répond pas aux nouvelles exigences de sécurité. Nous venons d'envoyer un message de réinitialisation de mot de passe à l'adresse courriel associée à ce compte. Merci de nous aider à protéger vos données.",
|
||||
"account.locked.out.message.1": "Pour protéger votre compte, il a été temporairement verrouillé. Réessayez dans 30 minutes.",
|
||||
"enterprise.login.btn.text": "Informations d'identification de la compagnie ou de l'école",
|
||||
"username.or.email.format.validation.less.chars.message": "Le nom d'utilisateur ou l'adresse courriel doit comporter au moins 3 caractères.",
|
||||
"username.or.email.format.validation.less.chars.message": "Le nom d'utilisateur ou l'adresse courriel doit comporter au moins 2 caractères.",
|
||||
"email.validation.message": "Entrez votre nom d'utilisateur ou votre adresse courriel",
|
||||
"password.validation.message": "Les critères de mot de passe n'ont pas été remplis",
|
||||
"account.activation.success.message.title": "Succès! Vous avez activé votre compte.",
|
||||
@@ -113,12 +106,13 @@
|
||||
"recommendation.page.title": "Recommandations | {siteName}",
|
||||
"recommendation.page.heading": "Nous avons quelques recommandations pour vous aider à démarrer.",
|
||||
"recommendation.skip.button": "Ignorer pour l'instant",
|
||||
"recommendation.option.trending": "Tendance",
|
||||
"recommendation.option.trending": "Tendances actuelles",
|
||||
"recommendation.option.popular": "Le plus populaire",
|
||||
"recommendation.option.recommended.for.you": "Recommandé pour vous",
|
||||
"recommendation.product-card.pill-text.course": "Cours",
|
||||
"recommendation.product-card.pill-text.professional-certificate": "Attestation professionnelle",
|
||||
"recommendation.product-card.pill-text.emeritus": "Offert à titre émérite",
|
||||
"recommendation.product-card.pill-text.shorelight": "Offert par Shorelight",
|
||||
"recommendation.product-card.pill-text.shorelight": "Offert via Shorelight",
|
||||
"recommendation.product-card.footer-text.number-of-courses": "{length} {label}",
|
||||
"recommendation.product-card.footer-text.subscription": "Abonnement",
|
||||
"recommendation.product-card.launch-icon.sr-text": "Ouvre un lien dans un nouvel onglet",
|
||||
@@ -160,9 +154,6 @@
|
||||
"terms.of.service": "Conditions générales du service",
|
||||
"registration.username.suggestion.label": "Suggéré :",
|
||||
"did.you.mean.alert.text": "Vouliez-vous dire",
|
||||
"register.page.terms.of.service.and.honor.code": "En créant un compte, vous acceptez le {tosAndHonorCode} et vous reconnaissez que {platformName} et chaque\n membre peut traiter vos données personnelles conformément à la {privacyPolicy}.",
|
||||
"register.page.honor.code": "J'accepte le {tosAndHonorCode} {platformName}",
|
||||
"register.page.terms.of.service": "J'accepte les {termsOfService} {platformName}",
|
||||
"sign.in": "Connexion",
|
||||
"reset.password.page.title": "Réinitialiser le mot de passe | {siteName}",
|
||||
"reset.password": "Réinitialiser le mot de passe",
|
||||
@@ -176,5 +167,15 @@
|
||||
"reset.server.rate.limit.error": "Trop de demandes.",
|
||||
"reset.password.success.heading": "Réinitialisation du mot de passe complétée.",
|
||||
"reset.password.success": "Votre mot de passe a été réinitialisé. Connectez-vous à votre compte.",
|
||||
"rate.limit.error": "Une erreur s'est produite en raison d'un trop grand nombre de demandes. Veuillez réessayer après un certain temps."
|
||||
}
|
||||
"rate.limit.error": "Une erreur s'est produite en raison d'un trop grand nombre de demandes. Veuillez réessayer après un certain temps.",
|
||||
"start.learning": "Commencez à apprendre",
|
||||
"with.site.name": "avec {siteName}",
|
||||
"your.career.turning.point": "Un tournant dans votre carrière",
|
||||
"is.here": "est là.",
|
||||
"welcome.to.platform": "Bienvenue sur {siteName}, {username}!",
|
||||
"complete.your.profile.1": "Complet",
|
||||
"complete.your.profile.2": "votre profil",
|
||||
"register.page.terms.of.service.and.honor.code": "En créant un compte, vous acceptez le {tosAndHonorCode} et vous reconnaissez que {platformName} et chaque membre traitent vos données personnelles conformément au {privacyPolicy}.",
|
||||
"register.page.honor.code": "J'accepte le {tosAndHonorCode} {platformName}",
|
||||
"register.page.terms.of.service": "J'accepte les {termsOfService} {platformName}"
|
||||
}
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
{
|
||||
"start.learning": "Start learning",
|
||||
"with.site.name": "with {siteName}",
|
||||
"your.career.turning.point": "Your career turning point",
|
||||
"is.here": "is here.",
|
||||
"welcome.to.platform": "Welcome to {siteName}, {username}!",
|
||||
"complete.your.profile.1": "Complete",
|
||||
"complete.your.profile.2": "your profile",
|
||||
"error.notfound.message": "The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again.",
|
||||
"institution.login.page.sub.heading": "Choose your institution from the list below",
|
||||
"logistration.sign.in": "Sign in",
|
||||
"logistration.register": "Register",
|
||||
@@ -26,8 +20,7 @@
|
||||
"registration.using.tpa.form.heading": "Finish creating your account",
|
||||
"zendesk.supportTitle": "edX Support",
|
||||
"zendesk.selectTicketForm": "Please choose your request type:",
|
||||
"error.notfound.message": "The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again.",
|
||||
"forgot.password.confirmation.message": "We sent an email to {email} with instructions to reset your password.\n If you do not receive a password reset message after 1 minute, verify that you entered\n the correct email address, or check your spam folder. If you need further assistance, {supportLink}.",
|
||||
"forgot.password.confirmation.message": "We sent an email to {email} with instructions to reset your password. If you do not receive a password reset message after 1 minute, verify that you entered the correct email address, or check your spam folder. If you need further assistance, {supportLink}.",
|
||||
"forgot.password.page.title": "Forgot Password | {siteName}",
|
||||
"forgot.password.page.heading": "Reset password",
|
||||
"forgot.password.page.instructions": "Please enter your email address below and we will send you an email with instructions on how to reset your password.",
|
||||
@@ -42,7 +35,7 @@
|
||||
"confirmation.message.title": "Check your email",
|
||||
"confirmation.support.link": "contact technical support",
|
||||
"need.help.sign.in.text": "Need help signing in?",
|
||||
"additional.help.text": "For additional help, contact {platformName} support at ",
|
||||
"additional.help.text": "For additional help, contact {platformName} support at",
|
||||
"sign.in.text": "Sign in",
|
||||
"extend.field.errors": "{emailError} below.",
|
||||
"invalid.token.heading": "Invalid password reset link",
|
||||
@@ -53,9 +46,9 @@
|
||||
"token.validation.internal.sever.error": "An error has occurred. Try refreshing the page, or check your internet connection.",
|
||||
"internal.server.error": "An error has occurred. Try refreshing the page, or check your internet connection.",
|
||||
"account.activation.error.message": "Something went wrong, please {supportLink} to resolve this issue.",
|
||||
"login.inactive.user.error": "In order to sign in, you need to activate your account.{lineBreak}\n {lineBreak}We just sent an activation link to {email}. If you do not receive an email,\n check your spam folders or {supportLink}.",
|
||||
"login.inactive.user.error": "In order to sign in, you need to activate your account.{lineBreak} {lineBreak}We just sent an activation link to {email}. If you do not receive an email, check your spam folders or {supportLink}.",
|
||||
"allowed.domain.login.error": "As {allowedDomain} user, You must login with your {allowedDomain} {tpaLink}.",
|
||||
"login.incorrect.credentials.error.attempts.text.1": "The username, email or password you entered is incorrect. You have {remainingAttempts} more sign in\n attempts before your account is temporarily locked.",
|
||||
"login.incorrect.credentials.error.attempts.text.1": "The username, email or password you entered is incorrect. You have {remainingAttempts} more sign in attempts before your account is temporarily locked.",
|
||||
"login.incorrect.credentials.error.attempts.text.2": "If you've forgotten your password, {resetLink}",
|
||||
"account.locked.out.message.2": "To be on the safe side, you can {resetLink} before trying again.",
|
||||
"login.incorrect.credentials.error.with.reset.link": "The username, email, or password you entered is incorrect. Please try again or {resetLink}.",
|
||||
@@ -71,7 +64,7 @@
|
||||
"non.compliant.password.message": "Your current password does not meet the new security requirements. We just sent a password-reset message to the email address associated with this account. Thank you for helping us keep your data safe.",
|
||||
"account.locked.out.message.1": "To protect your account, it's been temporarily locked. Try again in 30 minutes.",
|
||||
"enterprise.login.btn.text": "Company or school credentials",
|
||||
"username.or.email.format.validation.less.chars.message": "Username or email must have at least 3 characters.",
|
||||
"username.or.email.format.validation.less.chars.message": "Username or email must have at least 2 characters.",
|
||||
"email.validation.message": "Enter your username or email",
|
||||
"password.validation.message": "Password criteria has not been met",
|
||||
"account.activation.success.message.title": "Success! You have activated your account.",
|
||||
@@ -113,8 +106,9 @@
|
||||
"recommendation.page.title": "Recommendations | {siteName}",
|
||||
"recommendation.page.heading": "We have a few recommendations to get you started.",
|
||||
"recommendation.skip.button": "Skip for now",
|
||||
"recommendation.option.trending": "Trending",
|
||||
"recommendation.option.trending": "Trending Now",
|
||||
"recommendation.option.popular": "Most Popular",
|
||||
"recommendation.option.recommended.for.you": "Recommended For You",
|
||||
"recommendation.product-card.pill-text.course": "Course",
|
||||
"recommendation.product-card.pill-text.professional-certificate": "Professional Certificate",
|
||||
"recommendation.product-card.pill-text.emeritus": "Offered on Emeritus",
|
||||
@@ -160,9 +154,6 @@
|
||||
"terms.of.service": "Terms of Service",
|
||||
"registration.username.suggestion.label": "Suggested:",
|
||||
"did.you.mean.alert.text": "Did you mean",
|
||||
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each\n Member process your personal data in accordance with the {privacyPolicy}.",
|
||||
"register.page.honor.code": "I agree to the {platformName} {tosAndHonorCode}",
|
||||
"register.page.terms.of.service": "I agree to the {platformName} {termsOfService}",
|
||||
"sign.in": "Sign in",
|
||||
"reset.password.page.title": "Reset Password | {siteName}",
|
||||
"reset.password": "Reset password",
|
||||
@@ -176,5 +167,15 @@
|
||||
"reset.server.rate.limit.error": "Too many requests.",
|
||||
"reset.password.success.heading": "Password reset complete.",
|
||||
"reset.password.success": "Your password has been reset. Sign in to your account.",
|
||||
"rate.limit.error": "An error has occurred because of too many requests. Please try again after some time."
|
||||
}
|
||||
"rate.limit.error": "An error has occurred because of too many requests. Please try again after some time.",
|
||||
"start.learning": "Start learning",
|
||||
"with.site.name": "with {siteName}",
|
||||
"your.career.turning.point": "Your career turning point",
|
||||
"is.here": "is here.",
|
||||
"welcome.to.platform": "Welcome to {siteName}, {username}!",
|
||||
"complete.your.profile.1": "Complete",
|
||||
"complete.your.profile.2": "your profile",
|
||||
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each Member process your personal data in accordance with the {privacyPolicy}.",
|
||||
"register.page.honor.code": "I agree to the {platformName} {tosAndHonorCode}",
|
||||
"register.page.terms.of.service": "I agree to the {platformName} {termsOfService}"
|
||||
}
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
{
|
||||
"start.learning": "Start learning",
|
||||
"with.site.name": "with {siteName}",
|
||||
"your.career.turning.point": "Your career turning point",
|
||||
"is.here": "is here.",
|
||||
"welcome.to.platform": "Welcome to {siteName}, {username}!",
|
||||
"complete.your.profile.1": "Complete",
|
||||
"complete.your.profile.2": "your profile",
|
||||
"error.notfound.message": "The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again.",
|
||||
"institution.login.page.sub.heading": "Choose your institution from the list below",
|
||||
"logistration.sign.in": "Sign in",
|
||||
"logistration.register": "Register",
|
||||
@@ -26,8 +20,7 @@
|
||||
"registration.using.tpa.form.heading": "Finish creating your account",
|
||||
"zendesk.supportTitle": "edX Support",
|
||||
"zendesk.selectTicketForm": "Please choose your request type:",
|
||||
"error.notfound.message": "The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again.",
|
||||
"forgot.password.confirmation.message": "We sent an email to {email} with instructions to reset your password.\n If you do not receive a password reset message after 1 minute, verify that you entered\n the correct email address, or check your spam folder. If you need further assistance, {supportLink}.",
|
||||
"forgot.password.confirmation.message": "We sent an email to {email} with instructions to reset your password. If you do not receive a password reset message after 1 minute, verify that you entered the correct email address, or check your spam folder. If you need further assistance, {supportLink}.",
|
||||
"forgot.password.page.title": "Forgot Password | {siteName}",
|
||||
"forgot.password.page.heading": "Reset password",
|
||||
"forgot.password.page.instructions": "Please enter your email address below and we will send you an email with instructions on how to reset your password.",
|
||||
@@ -42,7 +35,7 @@
|
||||
"confirmation.message.title": "Check your email",
|
||||
"confirmation.support.link": "contact technical support",
|
||||
"need.help.sign.in.text": "Need help signing in?",
|
||||
"additional.help.text": "For additional help, contact {platformName} support at ",
|
||||
"additional.help.text": "For additional help, contact {platformName} support at",
|
||||
"sign.in.text": "Sign in",
|
||||
"extend.field.errors": "{emailError} below.",
|
||||
"invalid.token.heading": "Invalid password reset link",
|
||||
@@ -53,9 +46,9 @@
|
||||
"token.validation.internal.sever.error": "An error has occurred. Try refreshing the page, or check your internet connection.",
|
||||
"internal.server.error": "An error has occurred. Try refreshing the page, or check your internet connection.",
|
||||
"account.activation.error.message": "Something went wrong, please {supportLink} to resolve this issue.",
|
||||
"login.inactive.user.error": "In order to sign in, you need to activate your account.{lineBreak}\n {lineBreak}We just sent an activation link to {email}. If you do not receive an email,\n check your spam folders or {supportLink}.",
|
||||
"login.inactive.user.error": "In order to sign in, you need to activate your account.{lineBreak} {lineBreak}We just sent an activation link to {email}. If you do not receive an email, check your spam folders or {supportLink}.",
|
||||
"allowed.domain.login.error": "As {allowedDomain} user, You must login with your {allowedDomain} {tpaLink}.",
|
||||
"login.incorrect.credentials.error.attempts.text.1": "The username, email or password you entered is incorrect. You have {remainingAttempts} more sign in\n attempts before your account is temporarily locked.",
|
||||
"login.incorrect.credentials.error.attempts.text.1": "The username, email or password you entered is incorrect. You have {remainingAttempts} more sign in attempts before your account is temporarily locked.",
|
||||
"login.incorrect.credentials.error.attempts.text.2": "If you've forgotten your password, {resetLink}",
|
||||
"account.locked.out.message.2": "To be on the safe side, you can {resetLink} before trying again.",
|
||||
"login.incorrect.credentials.error.with.reset.link": "The username, email, or password you entered is incorrect. Please try again or {resetLink}.",
|
||||
@@ -71,7 +64,7 @@
|
||||
"non.compliant.password.message": "Your current password does not meet the new security requirements. We just sent a password-reset message to the email address associated with this account. Thank you for helping us keep your data safe.",
|
||||
"account.locked.out.message.1": "To protect your account, it's been temporarily locked. Try again in 30 minutes.",
|
||||
"enterprise.login.btn.text": "Company or school credentials",
|
||||
"username.or.email.format.validation.less.chars.message": "Username or email must have at least 3 characters.",
|
||||
"username.or.email.format.validation.less.chars.message": "Username or email must have at least 2 characters.",
|
||||
"email.validation.message": "Enter your username or email",
|
||||
"password.validation.message": "Password criteria has not been met",
|
||||
"account.activation.success.message.title": "Success! You have activated your account.",
|
||||
@@ -113,8 +106,9 @@
|
||||
"recommendation.page.title": "Recommendations | {siteName}",
|
||||
"recommendation.page.heading": "We have a few recommendations to get you started.",
|
||||
"recommendation.skip.button": "Skip for now",
|
||||
"recommendation.option.trending": "Trending",
|
||||
"recommendation.option.trending": "Trending Now",
|
||||
"recommendation.option.popular": "Most Popular",
|
||||
"recommendation.option.recommended.for.you": "Recommended For You",
|
||||
"recommendation.product-card.pill-text.course": "Course",
|
||||
"recommendation.product-card.pill-text.professional-certificate": "Professional Certificate",
|
||||
"recommendation.product-card.pill-text.emeritus": "Offered on Emeritus",
|
||||
@@ -160,9 +154,6 @@
|
||||
"terms.of.service": "Terms of Service",
|
||||
"registration.username.suggestion.label": "Suggested:",
|
||||
"did.you.mean.alert.text": "Did you mean",
|
||||
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each\n Member process your personal data in accordance with the {privacyPolicy}.",
|
||||
"register.page.honor.code": "I agree to the {platformName} {tosAndHonorCode}",
|
||||
"register.page.terms.of.service": "I agree to the {platformName} {termsOfService}",
|
||||
"sign.in": "Sign in",
|
||||
"reset.password.page.title": "Reset Password | {siteName}",
|
||||
"reset.password": "Reset password",
|
||||
@@ -176,5 +167,15 @@
|
||||
"reset.server.rate.limit.error": "Too many requests.",
|
||||
"reset.password.success.heading": "Password reset complete.",
|
||||
"reset.password.success": "Your password has been reset. Sign in to your account.",
|
||||
"rate.limit.error": "An error has occurred because of too many requests. Please try again after some time."
|
||||
}
|
||||
"rate.limit.error": "An error has occurred because of too many requests. Please try again after some time.",
|
||||
"start.learning": "Start learning",
|
||||
"with.site.name": "with {siteName}",
|
||||
"your.career.turning.point": "Your career turning point",
|
||||
"is.here": "is here.",
|
||||
"welcome.to.platform": "Welcome to {siteName}, {username}!",
|
||||
"complete.your.profile.1": "Complete",
|
||||
"complete.your.profile.2": "your profile",
|
||||
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each Member process your personal data in accordance with the {privacyPolicy}.",
|
||||
"register.page.honor.code": "I agree to the {platformName} {tosAndHonorCode}",
|
||||
"register.page.terms.of.service": "I agree to the {platformName} {termsOfService}"
|
||||
}
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
{
|
||||
"start.learning": "Inizia a imparare",
|
||||
"with.site.name": "con {siteName}",
|
||||
"your.career.turning.point": "Your career turning point",
|
||||
"is.here": "is here.",
|
||||
"welcome.to.platform": "Benvenuto in {siteName}, {username}!",
|
||||
"complete.your.profile.1": "Completata",
|
||||
"complete.your.profile.2": "Il tuo profilo",
|
||||
"error.notfound.message": "La pagina che stai cercando non è disponibile o si è verificato un errore nell'URL. Controlla l'URL e riprova. ",
|
||||
"institution.login.page.sub.heading": "Scegli il tuo istituto dall'elenco sottostante",
|
||||
"logistration.sign.in": "Accedi",
|
||||
"logistration.register": "Registrazione",
|
||||
@@ -26,8 +20,7 @@
|
||||
"registration.using.tpa.form.heading": "Completa la creazione del tuo account",
|
||||
"zendesk.supportTitle": "edX Support",
|
||||
"zendesk.selectTicketForm": "Please choose your request type:",
|
||||
"error.notfound.message": "La pagina che stai cercando non è disponibile o si è verificato un errore nell'URL. Controlla l'URL e riprova. ",
|
||||
"forgot.password.confirmation.message": "Abbiamo inviato un'email a {email} con le istruzioni per reimpostare la password. Se non ricevi un messaggio di reimpostazione della password dopo 1 minuto, verifica di aver inserito l'indirizzo e-mail corretto o controlla la cartella spam. Se hai bisogno di ulteriore assistenza, {supportLink}.",
|
||||
"forgot.password.confirmation.message": "We sent an email to {email} with instructions to reset your password. If you do not receive a password reset message after 1 minute, verify that you entered the correct email address, or check your spam folder. If you need further assistance, {supportLink}.",
|
||||
"forgot.password.page.title": "Dimenticato la password | {siteName}",
|
||||
"forgot.password.page.heading": "Resetta la password",
|
||||
"forgot.password.page.instructions": "Inserisci il tuo indirizzo e-mail qui sotto e ti invieremo un'e-mail con le istruzioni su come reimpostare la tua password.",
|
||||
@@ -42,7 +35,7 @@
|
||||
"confirmation.message.title": "Controlla la tua casella di posta",
|
||||
"confirmation.support.link": "contatta il supporto tecnico",
|
||||
"need.help.sign.in.text": "Hai bisogno di aiuto per l'accesso? ",
|
||||
"additional.help.text": "Per ulteriore assistenza, contattare l'assistenza {platformName} all'indirizzo",
|
||||
"additional.help.text": "For additional help, contact {platformName} support at",
|
||||
"sign.in.text": "Accedi",
|
||||
"extend.field.errors": "{emailError} di seguito.",
|
||||
"invalid.token.heading": "Link di ripristino della password non valido",
|
||||
@@ -53,9 +46,9 @@
|
||||
"token.validation.internal.sever.error": "Si è verificato un errore. Prova ad aggiornare la pagina oppure verifica la connessione internet.",
|
||||
"internal.server.error": "Si è verificato un errore. Prova ad aggiornare la pagina oppure verifica la connessione internet.",
|
||||
"account.activation.error.message": "Si è verificato un errore, seleziona {supportLink} per risolvere il problema. ",
|
||||
"login.inactive.user.error": "Per accedere, devi attivare il tuo account.{lineBreak} {lineBreak}Abbiamo appena inviato un link di attivazione a {email}. Se non ricevi un'email, controlla la cartella della posta indesiderata oppure seleziona {supportLink}.",
|
||||
"login.inactive.user.error": "In order to sign in, you need to activate your account.{lineBreak} {lineBreak}We just sent an activation link to {email}. If you do not receive an email, check your spam folders or {supportLink}.",
|
||||
"allowed.domain.login.error": "As {allowedDomain} user, You must login with your {allowedDomain} {tpaLink}.",
|
||||
"login.incorrect.credentials.error.attempts.text.1": "Il nome utente, l'e-mail o la password che hai inserito non sono corretti. Hai {remainingAttempts} più tentativi di accesso prima che il tuo account venga temporaneamente bloccato.",
|
||||
"login.incorrect.credentials.error.attempts.text.1": "The username, email or password you entered is incorrect. You have {remainingAttempts} more sign in attempts before your account is temporarily locked.",
|
||||
"login.incorrect.credentials.error.attempts.text.2": "Se hai dimenticato la password, {resetLink}",
|
||||
"account.locked.out.message.2": "Per sicurezza, puoi {resetLink} prima di riprovare.",
|
||||
"login.incorrect.credentials.error.with.reset.link": "Il nome utente, l'e-mail o la password inseriti non sono corretti. Riprova o {resetLink}.",
|
||||
@@ -71,7 +64,7 @@
|
||||
"non.compliant.password.message": "La tua password attuale non soddisfa i nuovi requisiti di sicurezza. Abbiamo appena inviato un messaggio di reimpostazione della password all'indirizzo e-mail associato a questo account. Grazie per averci aiutato a mantenere i tuoi dati al sicuro.",
|
||||
"account.locked.out.message.1": "Per proteggere il tuo account, è stato temporaneamente bloccato. Riprova tra 30 minuti.",
|
||||
"enterprise.login.btn.text": "Credenziali aziendali o scolastiche",
|
||||
"username.or.email.format.validation.less.chars.message": "Il nome utente o l'e-mail deve contenere almeno 3 caratteri.",
|
||||
"username.or.email.format.validation.less.chars.message": "Il nome utente o l'e-mail deve contenere almeno 2 caratteri.",
|
||||
"email.validation.message": "Inserisci il tuo nome utente o e-mail",
|
||||
"password.validation.message": "I criteri della password non sono stati soddisfatti",
|
||||
"account.activation.success.message.title": "Completato correttamente! Hai attivato il tuo account. ",
|
||||
@@ -113,8 +106,9 @@
|
||||
"recommendation.page.title": "Recommendations | {siteName}",
|
||||
"recommendation.page.heading": "We have a few recommendations to get you started.",
|
||||
"recommendation.skip.button": "Skip for now",
|
||||
"recommendation.option.trending": "Trending",
|
||||
"recommendation.option.trending": "Trending Now",
|
||||
"recommendation.option.popular": "Most Popular",
|
||||
"recommendation.option.recommended.for.you": "Recommended For You",
|
||||
"recommendation.product-card.pill-text.course": "Course",
|
||||
"recommendation.product-card.pill-text.professional-certificate": "Professional Certificate",
|
||||
"recommendation.product-card.pill-text.emeritus": "Offered on Emeritus",
|
||||
@@ -160,9 +154,6 @@
|
||||
"terms.of.service": "Termini di Servizio",
|
||||
"registration.username.suggestion.label": "Suggerito:",
|
||||
"did.you.mean.alert.text": "Intendevi",
|
||||
"register.page.terms.of.service.and.honor.code": "Creando un account, accetti il {tosAndHonorCode} e riconosci che {platformName} e ciascun Membro trattano i tuoi dati personali in conformità con l' {privacyPolicy}.",
|
||||
"register.page.honor.code": "Accetto {platformName} {tosAndHonorCode}",
|
||||
"register.page.terms.of.service": "Accetto {platformName} {termsOfService}",
|
||||
"sign.in": "Accedi",
|
||||
"reset.password.page.title": "Ripristina password | {siteName}",
|
||||
"reset.password": "Resetta la password",
|
||||
@@ -176,5 +167,15 @@
|
||||
"reset.server.rate.limit.error": "Troppe richieste.",
|
||||
"reset.password.success.heading": "Ripristino della password completato.",
|
||||
"reset.password.success": "La tua password è stata resettata. Accedi al tuo account.",
|
||||
"rate.limit.error": "Si è verificato un errore dovuto alle troppe richieste. Prova di nuovo più tardi."
|
||||
}
|
||||
"rate.limit.error": "Si è verificato un errore dovuto alle troppe richieste. Prova di nuovo più tardi.",
|
||||
"start.learning": "Inizia a imparare",
|
||||
"with.site.name": "con {siteName}",
|
||||
"your.career.turning.point": "Your career turning point",
|
||||
"is.here": "is here.",
|
||||
"welcome.to.platform": "Benvenuto in {siteName}, {username}!",
|
||||
"complete.your.profile.1": "Completata",
|
||||
"complete.your.profile.2": "Il tuo profilo",
|
||||
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each Member process your personal data in accordance with the {privacyPolicy}.",
|
||||
"register.page.honor.code": "I agree to the {platformName} {tosAndHonorCode}",
|
||||
"register.page.terms.of.service": "I agree to the {platformName} {termsOfService}"
|
||||
}
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
{
|
||||
"start.learning": "Start learning",
|
||||
"with.site.name": "with {siteName}",
|
||||
"your.career.turning.point": "Your career turning point",
|
||||
"is.here": "is here.",
|
||||
"welcome.to.platform": "Welcome to {siteName}, {username}!",
|
||||
"complete.your.profile.1": "Complete",
|
||||
"complete.your.profile.2": "your profile",
|
||||
"error.notfound.message": "The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again.",
|
||||
"institution.login.page.sub.heading": "Choose your institution from the list below",
|
||||
"logistration.sign.in": "Sign in",
|
||||
"logistration.register": "Register",
|
||||
@@ -26,8 +20,7 @@
|
||||
"registration.using.tpa.form.heading": "Finish creating your account",
|
||||
"zendesk.supportTitle": "edX Support",
|
||||
"zendesk.selectTicketForm": "Please choose your request type:",
|
||||
"error.notfound.message": "The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again.",
|
||||
"forgot.password.confirmation.message": "We sent an email to {email} with instructions to reset your password.\n If you do not receive a password reset message after 1 minute, verify that you entered\n the correct email address, or check your spam folder. If you need further assistance, {supportLink}.",
|
||||
"forgot.password.confirmation.message": "We sent an email to {email} with instructions to reset your password. If you do not receive a password reset message after 1 minute, verify that you entered the correct email address, or check your spam folder. If you need further assistance, {supportLink}.",
|
||||
"forgot.password.page.title": "Forgot Password | {siteName}",
|
||||
"forgot.password.page.heading": "Reset password",
|
||||
"forgot.password.page.instructions": "Please enter your email address below and we will send you an email with instructions on how to reset your password.",
|
||||
@@ -42,7 +35,7 @@
|
||||
"confirmation.message.title": "Check your email",
|
||||
"confirmation.support.link": "contact technical support",
|
||||
"need.help.sign.in.text": "Need help signing in?",
|
||||
"additional.help.text": "For additional help, contact {platformName} support at ",
|
||||
"additional.help.text": "For additional help, contact {platformName} support at",
|
||||
"sign.in.text": "Sign in",
|
||||
"extend.field.errors": "{emailError} below.",
|
||||
"invalid.token.heading": "Invalid password reset link",
|
||||
@@ -53,9 +46,9 @@
|
||||
"token.validation.internal.sever.error": "An error has occurred. Try refreshing the page, or check your internet connection.",
|
||||
"internal.server.error": "An error has occurred. Try refreshing the page, or check your internet connection.",
|
||||
"account.activation.error.message": "Something went wrong, please {supportLink} to resolve this issue.",
|
||||
"login.inactive.user.error": "In order to sign in, you need to activate your account.{lineBreak}\n {lineBreak}We just sent an activation link to {email}. If you do not receive an email,\n check your spam folders or {supportLink}.",
|
||||
"login.inactive.user.error": "In order to sign in, you need to activate your account.{lineBreak} {lineBreak}We just sent an activation link to {email}. If you do not receive an email, check your spam folders or {supportLink}.",
|
||||
"allowed.domain.login.error": "As {allowedDomain} user, You must login with your {allowedDomain} {tpaLink}.",
|
||||
"login.incorrect.credentials.error.attempts.text.1": "The username, email or password you entered is incorrect. You have {remainingAttempts} more sign in\n attempts before your account is temporarily locked.",
|
||||
"login.incorrect.credentials.error.attempts.text.1": "The username, email or password you entered is incorrect. You have {remainingAttempts} more sign in attempts before your account is temporarily locked.",
|
||||
"login.incorrect.credentials.error.attempts.text.2": "If you've forgotten your password, {resetLink}",
|
||||
"account.locked.out.message.2": "To be on the safe side, you can {resetLink} before trying again.",
|
||||
"login.incorrect.credentials.error.with.reset.link": "The username, email, or password you entered is incorrect. Please try again or {resetLink}.",
|
||||
@@ -71,7 +64,7 @@
|
||||
"non.compliant.password.message": "Your current password does not meet the new security requirements. We just sent a password-reset message to the email address associated with this account. Thank you for helping us keep your data safe.",
|
||||
"account.locked.out.message.1": "To protect your account, it's been temporarily locked. Try again in 30 minutes.",
|
||||
"enterprise.login.btn.text": "Company or school credentials",
|
||||
"username.or.email.format.validation.less.chars.message": "Username or email must have at least 3 characters.",
|
||||
"username.or.email.format.validation.less.chars.message": "Username or email must have at least 2 characters.",
|
||||
"email.validation.message": "Enter your username or email",
|
||||
"password.validation.message": "Password criteria has not been met",
|
||||
"account.activation.success.message.title": "Success! You have activated your account.",
|
||||
@@ -113,8 +106,9 @@
|
||||
"recommendation.page.title": "Recommendations | {siteName}",
|
||||
"recommendation.page.heading": "We have a few recommendations to get you started.",
|
||||
"recommendation.skip.button": "Skip for now",
|
||||
"recommendation.option.trending": "Trending",
|
||||
"recommendation.option.trending": "Trending Now",
|
||||
"recommendation.option.popular": "Most Popular",
|
||||
"recommendation.option.recommended.for.you": "Recommended For You",
|
||||
"recommendation.product-card.pill-text.course": "Course",
|
||||
"recommendation.product-card.pill-text.professional-certificate": "Professional Certificate",
|
||||
"recommendation.product-card.pill-text.emeritus": "Offered on Emeritus",
|
||||
@@ -160,9 +154,6 @@
|
||||
"terms.of.service": "Terms of Service",
|
||||
"registration.username.suggestion.label": "Suggested:",
|
||||
"did.you.mean.alert.text": "Did you mean",
|
||||
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each\n Member process your personal data in accordance with the {privacyPolicy}.",
|
||||
"register.page.honor.code": "I agree to the {platformName} {tosAndHonorCode}",
|
||||
"register.page.terms.of.service": "I agree to the {platformName} {termsOfService}",
|
||||
"sign.in": "Sign in",
|
||||
"reset.password.page.title": "Reset Password | {siteName}",
|
||||
"reset.password": "Reset password",
|
||||
@@ -176,5 +167,15 @@
|
||||
"reset.server.rate.limit.error": "Too many requests.",
|
||||
"reset.password.success.heading": "Password reset complete.",
|
||||
"reset.password.success": "Your password has been reset. Sign in to your account.",
|
||||
"rate.limit.error": "An error has occurred because of too many requests. Please try again after some time."
|
||||
}
|
||||
"rate.limit.error": "An error has occurred because of too many requests. Please try again after some time.",
|
||||
"start.learning": "Start learning",
|
||||
"with.site.name": "with {siteName}",
|
||||
"your.career.turning.point": "Your career turning point",
|
||||
"is.here": "is here.",
|
||||
"welcome.to.platform": "Welcome to {siteName}, {username}!",
|
||||
"complete.your.profile.1": "Complete",
|
||||
"complete.your.profile.2": "your profile",
|
||||
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each Member process your personal data in accordance with the {privacyPolicy}.",
|
||||
"register.page.honor.code": "I agree to the {platformName} {tosAndHonorCode}",
|
||||
"register.page.terms.of.service": "I agree to the {platformName} {termsOfService}"
|
||||
}
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
{
|
||||
"start.learning": "Começar a aprender",
|
||||
"with.site.name": "com {siteName}",
|
||||
"your.career.turning.point": "Your career turning point",
|
||||
"is.here": "is here.",
|
||||
"welcome.to.platform": "Bem vindo a {siteName}, {username}!",
|
||||
"complete.your.profile.1": "Concluído",
|
||||
"complete.your.profile.2": "o seu perfil",
|
||||
"error.notfound.message": "A página que procura não está disponível ou há um erro no URL. Por favor, verifique o URL e tente novamente.",
|
||||
"institution.login.page.sub.heading": "Escolha a sua instituição a partir da lista abaixo",
|
||||
"logistration.sign.in": "Iniciar sessão",
|
||||
"logistration.register": "Registe-se",
|
||||
@@ -26,8 +20,7 @@
|
||||
"registration.using.tpa.form.heading": "Acabe de criar a sua conta",
|
||||
"zendesk.supportTitle": "Apoio edX",
|
||||
"zendesk.selectTicketForm": "Por favor, escolha o seu tipo de pedido:",
|
||||
"error.notfound.message": "A página que procura não está disponível ou há um erro no URL. Por favor, verifique o URL e tente novamente.",
|
||||
"forgot.password.confirmation.message": "Enviámos um email para {email} com instruções para redefinir a sua palavra-passe.\n Se não receber uma mensagem de redefinição de palavra-passe após 1 minuto, verifique se introduziu\n o endereço de correio electrónico correto, ou verifique a sua pasta de spam. Se precisar de mais assistência, {supportLink}.",
|
||||
"forgot.password.confirmation.message": "We sent an email to {email} with instructions to reset your password. If you do not receive a password reset message after 1 minute, verify that you entered the correct email address, or check your spam folder. If you need further assistance, {supportLink}.",
|
||||
"forgot.password.page.title": "Esqueceu a Senha | {siteName}",
|
||||
"forgot.password.page.heading": "Redefinir palavra-passe",
|
||||
"forgot.password.page.instructions": "Por favor introduza o seu endereço de email abaixo e enviar-lhe-emos um email com instruções sobre como redefinir a sua palavra-passe.",
|
||||
@@ -42,7 +35,7 @@
|
||||
"confirmation.message.title": "Verifique o seu email",
|
||||
"confirmation.support.link": "contacto o suporte técnico",
|
||||
"need.help.sign.in.text": "Precisa de ajuda para entrar?",
|
||||
"additional.help.text": "Para ajuda adicional, contacte o apoio {platformName} em",
|
||||
"additional.help.text": "For additional help, contact {platformName} support at",
|
||||
"sign.in.text": "Iniciar sessão",
|
||||
"extend.field.errors": "{emailError} abaixo.",
|
||||
"invalid.token.heading": "Link para Redefinir Palavra-passe inválido",
|
||||
@@ -53,9 +46,9 @@
|
||||
"token.validation.internal.sever.error": "Ocorreu um erro. Tente actualizar a página, ou verifique a sua ligação à Internet.",
|
||||
"internal.server.error": "Ocorreu um erro. Tente actualizar a página, ou verifique a sua ligação à Internet.",
|
||||
"account.activation.error.message": "Alguma coisa correu mal, siga {supportLink} para resolver esta questão.",
|
||||
"login.inactive.user.error": "Para iniciar sessão, precisa ativar a sua conta. {lineBreak}\n {lineBreak} Acabámos de enviar um link de ativação para {email}. Se não receber um e-mail,\n verifique as suas pastas de spam ou {supportLink}.",
|
||||
"login.inactive.user.error": "In order to sign in, you need to activate your account.{lineBreak} {lineBreak}We just sent an activation link to {email}. If you do not receive an email, check your spam folders or {supportLink}.",
|
||||
"allowed.domain.login.error": "Como utilizador do {allowedDomain}, deve iniciar sessão com o seu {allowedDomain} {tpaLink}.",
|
||||
"login.incorrect.credentials.error.attempts.text.1": "O nome de utilizador, email ou palavra-passe que introduziu está incorreto. Tem {remainingAttempts} mais tentativas\n de login antes da sua conta ser temporariamente bloqueada.",
|
||||
"login.incorrect.credentials.error.attempts.text.1": "The username, email or password you entered is incorrect. You have {remainingAttempts} more sign in attempts before your account is temporarily locked.",
|
||||
"login.incorrect.credentials.error.attempts.text.2": "Se esqueceu a sua palavra-passe, {resetLink}",
|
||||
"account.locked.out.message.2": "Por precaução, pode {resetLink} antes de tentar novamente.",
|
||||
"login.incorrect.credentials.error.with.reset.link": "O nome de utilizador, email ou senha que introduziu está incorreto. Por favor, tente novamente ou {resetLink}.",
|
||||
@@ -71,7 +64,7 @@
|
||||
"non.compliant.password.message": "A sua palavra-passe atual não satisfaz os novos requisitos de segurança. Acabámos de enviar uma mensagem de redefinição da palavra-passe para o endereço de email associado a esta conta. Obrigado por nos ajudar a manter os seus dados em segurança.",
|
||||
"account.locked.out.message.1": "Para proteger sua conta, esta foi temporariamente bloqueada. Tente novamente dentro de 30 minutos.",
|
||||
"enterprise.login.btn.text": "Credenciais da empresa ou escola",
|
||||
"username.or.email.format.validation.less.chars.message": "O nome de utilizador ou email deve ter pelo menos 3 carateres.",
|
||||
"username.or.email.format.validation.less.chars.message": "O nome de utilizador ou email deve ter pelo menos 2 carateres.",
|
||||
"email.validation.message": "Insira o seu nome de utilizador ou email",
|
||||
"password.validation.message": "Os critérios de palavra-passe não foram cumpridos",
|
||||
"account.activation.success.message.title": "Sucesso! Você ativou a sua conta.",
|
||||
@@ -113,8 +106,9 @@
|
||||
"recommendation.page.title": "Recomendações | {siteName}",
|
||||
"recommendation.page.heading": "Temos algumas recomendações para o ajudar a começar.",
|
||||
"recommendation.skip.button": "Saltar por enquanto",
|
||||
"recommendation.option.trending": "Trending",
|
||||
"recommendation.option.trending": "Trending Now",
|
||||
"recommendation.option.popular": "Most Popular",
|
||||
"recommendation.option.recommended.for.you": "Recommended For You",
|
||||
"recommendation.product-card.pill-text.course": "Course",
|
||||
"recommendation.product-card.pill-text.professional-certificate": "Professional Certificate",
|
||||
"recommendation.product-card.pill-text.emeritus": "Offered on Emeritus",
|
||||
@@ -160,9 +154,6 @@
|
||||
"terms.of.service": "Termos do Serviço",
|
||||
"registration.username.suggestion.label": "Sugerido:",
|
||||
"did.you.mean.alert.text": "Quis dizer",
|
||||
"register.page.terms.of.service.and.honor.code": "Ao criar uma conta, concorda com o {tosAndHonorCode} e reconhece que {platformName} e cada\n Membro processa os seus dados pessoais em conformidade com a {privacyPolicy}.",
|
||||
"register.page.honor.code": "Concordo com a {tosAndHonorCode} {platformName}",
|
||||
"register.page.terms.of.service": "Concordo com os {termsOfService} {platformName}",
|
||||
"sign.in": "Iniciar sessão",
|
||||
"reset.password.page.title": "Redefinir Palavra-passe | {siteName}",
|
||||
"reset.password": "Redefinir palavra-passe",
|
||||
@@ -176,5 +167,15 @@
|
||||
"reset.server.rate.limit.error": "Demasiados pedidos.",
|
||||
"reset.password.success.heading": "Redefinição de palavra-passe concluída",
|
||||
"reset.password.success": "A sua palavra-passe foi redefinida. Inicie sessão na sua conta.",
|
||||
"rate.limit.error": "Ocorreu um erro devido a demasiados pedidos. Por favor, tente novamente após algum tempo."
|
||||
}
|
||||
"rate.limit.error": "Ocorreu um erro devido a demasiados pedidos. Por favor, tente novamente após algum tempo.",
|
||||
"start.learning": "Começar a aprender",
|
||||
"with.site.name": "com {siteName}",
|
||||
"your.career.turning.point": "Your career turning point",
|
||||
"is.here": "is here.",
|
||||
"welcome.to.platform": "Bem vindo a {siteName}, {username}!",
|
||||
"complete.your.profile.1": "Concluído",
|
||||
"complete.your.profile.2": "o seu perfil",
|
||||
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each Member process your personal data in accordance with the {privacyPolicy}.",
|
||||
"register.page.honor.code": "I agree to the {platformName} {tosAndHonorCode}",
|
||||
"register.page.terms.of.service": "I agree to the {platformName} {termsOfService}"
|
||||
}
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
{
|
||||
"start.learning": "Start learning",
|
||||
"with.site.name": "with {siteName}",
|
||||
"your.career.turning.point": "Your career turning point",
|
||||
"is.here": "is here.",
|
||||
"welcome.to.platform": "Welcome to {siteName}, {username}!",
|
||||
"complete.your.profile.1": "Complete",
|
||||
"complete.your.profile.2": "your profile",
|
||||
"error.notfound.message": "The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again.",
|
||||
"institution.login.page.sub.heading": "Choose your institution from the list below",
|
||||
"logistration.sign.in": "Sign in",
|
||||
"logistration.register": "Register",
|
||||
@@ -26,8 +20,7 @@
|
||||
"registration.using.tpa.form.heading": "Finish creating your account",
|
||||
"zendesk.supportTitle": "edX Support",
|
||||
"zendesk.selectTicketForm": "Please choose your request type:",
|
||||
"error.notfound.message": "The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again.",
|
||||
"forgot.password.confirmation.message": "We sent an email to {email} with instructions to reset your password.\n If you do not receive a password reset message after 1 minute, verify that you entered\n the correct email address, or check your spam folder. If you need further assistance, {supportLink}.",
|
||||
"forgot.password.confirmation.message": "We sent an email to {email} with instructions to reset your password. If you do not receive a password reset message after 1 minute, verify that you entered the correct email address, or check your spam folder. If you need further assistance, {supportLink}.",
|
||||
"forgot.password.page.title": "Forgot Password | {siteName}",
|
||||
"forgot.password.page.heading": "Reset password",
|
||||
"forgot.password.page.instructions": "Please enter your email address below and we will send you an email with instructions on how to reset your password.",
|
||||
@@ -42,7 +35,7 @@
|
||||
"confirmation.message.title": "Check your email",
|
||||
"confirmation.support.link": "contact technical support",
|
||||
"need.help.sign.in.text": "Need help signing in?",
|
||||
"additional.help.text": "For additional help, contact {platformName} support at ",
|
||||
"additional.help.text": "For additional help, contact {platformName} support at",
|
||||
"sign.in.text": "Sign in",
|
||||
"extend.field.errors": "{emailError} below.",
|
||||
"invalid.token.heading": "Invalid password reset link",
|
||||
@@ -53,9 +46,9 @@
|
||||
"token.validation.internal.sever.error": "An error has occurred. Try refreshing the page, or check your internet connection.",
|
||||
"internal.server.error": "An error has occurred. Try refreshing the page, or check your internet connection.",
|
||||
"account.activation.error.message": "Something went wrong, please {supportLink} to resolve this issue.",
|
||||
"login.inactive.user.error": "In order to sign in, you need to activate your account.{lineBreak}\n {lineBreak}We just sent an activation link to {email}. If you do not receive an email,\n check your spam folders or {supportLink}.",
|
||||
"login.inactive.user.error": "In order to sign in, you need to activate your account.{lineBreak} {lineBreak}We just sent an activation link to {email}. If you do not receive an email, check your spam folders or {supportLink}.",
|
||||
"allowed.domain.login.error": "As {allowedDomain} user, You must login with your {allowedDomain} {tpaLink}.",
|
||||
"login.incorrect.credentials.error.attempts.text.1": "The username, email or password you entered is incorrect. You have {remainingAttempts} more sign in\n attempts before your account is temporarily locked.",
|
||||
"login.incorrect.credentials.error.attempts.text.1": "The username, email or password you entered is incorrect. You have {remainingAttempts} more sign in attempts before your account is temporarily locked.",
|
||||
"login.incorrect.credentials.error.attempts.text.2": "If you've forgotten your password, {resetLink}",
|
||||
"account.locked.out.message.2": "To be on the safe side, you can {resetLink} before trying again.",
|
||||
"login.incorrect.credentials.error.with.reset.link": "The username, email, or password you entered is incorrect. Please try again or {resetLink}.",
|
||||
@@ -71,7 +64,7 @@
|
||||
"non.compliant.password.message": "Your current password does not meet the new security requirements. We just sent a password-reset message to the email address associated with this account. Thank you for helping us keep your data safe.",
|
||||
"account.locked.out.message.1": "To protect your account, it's been temporarily locked. Try again in 30 minutes.",
|
||||
"enterprise.login.btn.text": "Company or school credentials",
|
||||
"username.or.email.format.validation.less.chars.message": "Username or email must have at least 3 characters.",
|
||||
"username.or.email.format.validation.less.chars.message": "Username or email must have at least 2 characters.",
|
||||
"email.validation.message": "Enter your username or email",
|
||||
"password.validation.message": "Password criteria has not been met",
|
||||
"account.activation.success.message.title": "Success! You have activated your account.",
|
||||
@@ -113,8 +106,9 @@
|
||||
"recommendation.page.title": "Recommendations | {siteName}",
|
||||
"recommendation.page.heading": "We have a few recommendations to get you started.",
|
||||
"recommendation.skip.button": "Skip for now",
|
||||
"recommendation.option.trending": "Trending",
|
||||
"recommendation.option.trending": "Trending Now",
|
||||
"recommendation.option.popular": "Most Popular",
|
||||
"recommendation.option.recommended.for.you": "Recommended For You",
|
||||
"recommendation.product-card.pill-text.course": "Course",
|
||||
"recommendation.product-card.pill-text.professional-certificate": "Professional Certificate",
|
||||
"recommendation.product-card.pill-text.emeritus": "Offered on Emeritus",
|
||||
@@ -160,9 +154,6 @@
|
||||
"terms.of.service": "Terms of Service",
|
||||
"registration.username.suggestion.label": "Suggested:",
|
||||
"did.you.mean.alert.text": "Did you mean",
|
||||
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each\n Member process your personal data in accordance with the {privacyPolicy}.",
|
||||
"register.page.honor.code": "I agree to the {platformName} {tosAndHonorCode}",
|
||||
"register.page.terms.of.service": "I agree to the {platformName} {termsOfService}",
|
||||
"sign.in": "Sign in",
|
||||
"reset.password.page.title": "Reset Password | {siteName}",
|
||||
"reset.password": "Reset password",
|
||||
@@ -176,5 +167,15 @@
|
||||
"reset.server.rate.limit.error": "Too many requests.",
|
||||
"reset.password.success.heading": "Password reset complete.",
|
||||
"reset.password.success": "Your password has been reset. Sign in to your account.",
|
||||
"rate.limit.error": "An error has occurred because of too many requests. Please try again after some time."
|
||||
}
|
||||
"rate.limit.error": "An error has occurred because of too many requests. Please try again after some time.",
|
||||
"start.learning": "Start learning",
|
||||
"with.site.name": "with {siteName}",
|
||||
"your.career.turning.point": "Your career turning point",
|
||||
"is.here": "is here.",
|
||||
"welcome.to.platform": "Welcome to {siteName}, {username}!",
|
||||
"complete.your.profile.1": "Complete",
|
||||
"complete.your.profile.2": "your profile",
|
||||
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each Member process your personal data in accordance with the {privacyPolicy}.",
|
||||
"register.page.honor.code": "I agree to the {platformName} {tosAndHonorCode}",
|
||||
"register.page.terms.of.service": "I agree to the {platformName} {termsOfService}"
|
||||
}
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
{
|
||||
"start.learning": "Start learning",
|
||||
"with.site.name": "with {siteName}",
|
||||
"your.career.turning.point": "Your career turning point",
|
||||
"is.here": "is here.",
|
||||
"welcome.to.platform": "Welcome to {siteName}, {username}!",
|
||||
"complete.your.profile.1": "Complete",
|
||||
"complete.your.profile.2": "your profile",
|
||||
"error.notfound.message": "The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again.",
|
||||
"institution.login.page.sub.heading": "Choose your institution from the list below",
|
||||
"logistration.sign.in": "Sign in",
|
||||
"logistration.register": "Register",
|
||||
@@ -26,8 +20,7 @@
|
||||
"registration.using.tpa.form.heading": "Finish creating your account",
|
||||
"zendesk.supportTitle": "edX Support",
|
||||
"zendesk.selectTicketForm": "Please choose your request type:",
|
||||
"error.notfound.message": "The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again.",
|
||||
"forgot.password.confirmation.message": "We sent an email to {email} with instructions to reset your password.\n If you do not receive a password reset message after 1 minute, verify that you entered\n the correct email address, or check your spam folder. If you need further assistance, {supportLink}.",
|
||||
"forgot.password.confirmation.message": "We sent an email to {email} with instructions to reset your password. If you do not receive a password reset message after 1 minute, verify that you entered the correct email address, or check your spam folder. If you need further assistance, {supportLink}.",
|
||||
"forgot.password.page.title": "Forgot Password | {siteName}",
|
||||
"forgot.password.page.heading": "Reset password",
|
||||
"forgot.password.page.instructions": "Please enter your email address below and we will send you an email with instructions on how to reset your password.",
|
||||
@@ -42,7 +35,7 @@
|
||||
"confirmation.message.title": "Check your email",
|
||||
"confirmation.support.link": "contact technical support",
|
||||
"need.help.sign.in.text": "Need help signing in?",
|
||||
"additional.help.text": "For additional help, contact {platformName} support at ",
|
||||
"additional.help.text": "For additional help, contact {platformName} support at",
|
||||
"sign.in.text": "Sign in",
|
||||
"extend.field.errors": "{emailError} below.",
|
||||
"invalid.token.heading": "Invalid password reset link",
|
||||
@@ -53,9 +46,9 @@
|
||||
"token.validation.internal.sever.error": "An error has occurred. Try refreshing the page, or check your internet connection.",
|
||||
"internal.server.error": "An error has occurred. Try refreshing the page, or check your internet connection.",
|
||||
"account.activation.error.message": "Something went wrong, please {supportLink} to resolve this issue.",
|
||||
"login.inactive.user.error": "In order to sign in, you need to activate your account.{lineBreak}\n {lineBreak}We just sent an activation link to {email}. If you do not receive an email,\n check your spam folders or {supportLink}.",
|
||||
"login.inactive.user.error": "In order to sign in, you need to activate your account.{lineBreak} {lineBreak}We just sent an activation link to {email}. If you do not receive an email, check your spam folders or {supportLink}.",
|
||||
"allowed.domain.login.error": "As {allowedDomain} user, You must login with your {allowedDomain} {tpaLink}.",
|
||||
"login.incorrect.credentials.error.attempts.text.1": "The username, email or password you entered is incorrect. You have {remainingAttempts} more sign in\n attempts before your account is temporarily locked.",
|
||||
"login.incorrect.credentials.error.attempts.text.1": "The username, email or password you entered is incorrect. You have {remainingAttempts} more sign in attempts before your account is temporarily locked.",
|
||||
"login.incorrect.credentials.error.attempts.text.2": "If you've forgotten your password, {resetLink}",
|
||||
"account.locked.out.message.2": "To be on the safe side, you can {resetLink} before trying again.",
|
||||
"login.incorrect.credentials.error.with.reset.link": "The username, email, or password you entered is incorrect. Please try again or {resetLink}.",
|
||||
@@ -71,7 +64,7 @@
|
||||
"non.compliant.password.message": "Your current password does not meet the new security requirements. We just sent a password-reset message to the email address associated with this account. Thank you for helping us keep your data safe.",
|
||||
"account.locked.out.message.1": "To protect your account, it's been temporarily locked. Try again in 30 minutes.",
|
||||
"enterprise.login.btn.text": "Company or school credentials",
|
||||
"username.or.email.format.validation.less.chars.message": "Username or email must have at least 3 characters.",
|
||||
"username.or.email.format.validation.less.chars.message": "Username or email must have at least 2 characters.",
|
||||
"email.validation.message": "Enter your username or email",
|
||||
"password.validation.message": "Password criteria has not been met",
|
||||
"account.activation.success.message.title": "Success! You have activated your account.",
|
||||
@@ -113,8 +106,9 @@
|
||||
"recommendation.page.title": "Recommendations | {siteName}",
|
||||
"recommendation.page.heading": "We have a few recommendations to get you started.",
|
||||
"recommendation.skip.button": "Skip for now",
|
||||
"recommendation.option.trending": "Trending",
|
||||
"recommendation.option.trending": "Trending Now",
|
||||
"recommendation.option.popular": "Most Popular",
|
||||
"recommendation.option.recommended.for.you": "Recommended For You",
|
||||
"recommendation.product-card.pill-text.course": "Course",
|
||||
"recommendation.product-card.pill-text.professional-certificate": "Professional Certificate",
|
||||
"recommendation.product-card.pill-text.emeritus": "Offered on Emeritus",
|
||||
@@ -160,9 +154,6 @@
|
||||
"terms.of.service": "Terms of Service",
|
||||
"registration.username.suggestion.label": "Suggested:",
|
||||
"did.you.mean.alert.text": "Did you mean",
|
||||
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each\n Member process your personal data in accordance with the {privacyPolicy}.",
|
||||
"register.page.honor.code": "I agree to the {platformName} {tosAndHonorCode}",
|
||||
"register.page.terms.of.service": "I agree to the {platformName} {termsOfService}",
|
||||
"sign.in": "Sign in",
|
||||
"reset.password.page.title": "Reset Password | {siteName}",
|
||||
"reset.password": "Reset password",
|
||||
@@ -176,5 +167,15 @@
|
||||
"reset.server.rate.limit.error": "Too many requests.",
|
||||
"reset.password.success.heading": "Password reset complete.",
|
||||
"reset.password.success": "Your password has been reset. Sign in to your account.",
|
||||
"rate.limit.error": "An error has occurred because of too many requests. Please try again after some time."
|
||||
}
|
||||
"rate.limit.error": "An error has occurred because of too many requests. Please try again after some time.",
|
||||
"start.learning": "Start learning",
|
||||
"with.site.name": "with {siteName}",
|
||||
"your.career.turning.point": "Your career turning point",
|
||||
"is.here": "is here.",
|
||||
"welcome.to.platform": "Welcome to {siteName}, {username}!",
|
||||
"complete.your.profile.1": "Complete",
|
||||
"complete.your.profile.2": "your profile",
|
||||
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each Member process your personal data in accordance with the {privacyPolicy}.",
|
||||
"register.page.honor.code": "I agree to the {platformName} {tosAndHonorCode}",
|
||||
"register.page.terms.of.service": "I agree to the {platformName} {termsOfService}"
|
||||
}
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
{
|
||||
"start.learning": "开始学习",
|
||||
"with.site.name": "{siteName}",
|
||||
"your.career.turning.point": "Your career turning point",
|
||||
"is.here": "is here.",
|
||||
"welcome.to.platform": "欢迎来到{siteName},{username}!",
|
||||
"complete.your.profile.1": "完成",
|
||||
"complete.your.profile.2": "个人资料",
|
||||
"error.notfound.message": "您访问的地址不存在或有误。请检查URL后重新尝试访问。",
|
||||
"institution.login.page.sub.heading": "从下面的列表中选择您的机构",
|
||||
"logistration.sign.in": "登录",
|
||||
"logistration.register": "注册",
|
||||
@@ -26,8 +20,7 @@
|
||||
"registration.using.tpa.form.heading": "完成创建您的帐户",
|
||||
"zendesk.supportTitle": "edX Support",
|
||||
"zendesk.selectTicketForm": "Please choose your request type:",
|
||||
"error.notfound.message": "您访问的地址不存在或有误。请检查URL后重新尝试访问。",
|
||||
"forgot.password.confirmation.message": "我们向 {email} 发送了一封电子邮件,其中包含重置密码的说明。如果您在 1 分钟后没有收到密码重置消息,请确认您输入了正确的电子邮件地址,或者检查您的垃圾邮件文件夹。如果您需要进一步的帮助,点击{supportLink}。",
|
||||
"forgot.password.confirmation.message": "We sent an email to {email} with instructions to reset your password. If you do not receive a password reset message after 1 minute, verify that you entered the correct email address, or check your spam folder. If you need further assistance, {supportLink}.",
|
||||
"forgot.password.page.title": "忘记密码 | {siteName}",
|
||||
"forgot.password.page.heading": "重置密码",
|
||||
"forgot.password.page.instructions": "请在下面输入您的电子邮件地址,我们将向您发送一封电子邮件,其中包含有关如何重置密码的说明。",
|
||||
@@ -42,7 +35,7 @@
|
||||
"confirmation.message.title": "查收您的邮件",
|
||||
"confirmation.support.link": "联系技术支持",
|
||||
"need.help.sign.in.text": "需要帮助登录?",
|
||||
"additional.help.text": "如需更多帮助,请通过以下方式联系 {platformName} 支持",
|
||||
"additional.help.text": "For additional help, contact {platformName} support at",
|
||||
"sign.in.text": "登录",
|
||||
"extend.field.errors": "{emailError} 如下。",
|
||||
"invalid.token.heading": "密码重置链接无效",
|
||||
@@ -53,9 +46,9 @@
|
||||
"token.validation.internal.sever.error": "发生了错误。尝试刷新页面,或检查您的互联网连接。",
|
||||
"internal.server.error": "发生了错误。尝试刷新页面,或检查您的互联网连接。",
|
||||
"account.activation.error.message": "出了点问题,请联系{supportLink}解决这个问题。",
|
||||
"login.inactive.user.error": "若要登录,您需要激活您的帐户。{lineBreak} {lineBreak}我们刚刚向 {email} 发送了一个激活链接。如果您没有收到电子邮件,请检查您的垃圾邮件文件夹或 {supportLink}。",
|
||||
"login.inactive.user.error": "In order to sign in, you need to activate your account.{lineBreak} {lineBreak}We just sent an activation link to {email}. If you do not receive an email, check your spam folders or {supportLink}.",
|
||||
"allowed.domain.login.error": "作为 {allowedDomain} 用户,您必须使用 {allowedDomain} {tpaLink} 登录。",
|
||||
"login.incorrect.credentials.error.attempts.text.1": "您输入的用户名、电子邮件或密码不正确。在您的帐户被暂时锁定之前,您还有 {remainingAttempts} 次登录尝试。",
|
||||
"login.incorrect.credentials.error.attempts.text.1": "The username, email or password you entered is incorrect. You have {remainingAttempts} more sign in attempts before your account is temporarily locked.",
|
||||
"login.incorrect.credentials.error.attempts.text.2": "如果您忘记了密码,{resetLink}",
|
||||
"account.locked.out.message.2": "为了安全起见,您可以先{resetLink}再试一次。",
|
||||
"login.incorrect.credentials.error.with.reset.link": "您输入的用户名、电子邮件或密码不正确。请重试或 {resetLink}。",
|
||||
@@ -71,7 +64,7 @@
|
||||
"non.compliant.password.message": "您当前的密码不符合新的安全要求。我们刚刚向与此帐户关联的电子邮件地址发送了密码重置邮件。感谢您帮助我们保护您的数据安全。",
|
||||
"account.locked.out.message.1": "为了保护您的帐户,它已被暂时锁定。请在 30 分钟后重试。",
|
||||
"enterprise.login.btn.text": "单位或学校证书",
|
||||
"username.or.email.format.validation.less.chars.message": "用户名或电子邮件必须至少包含 3 个字符。",
|
||||
"username.or.email.format.validation.less.chars.message": "用户名或电子邮件必须至少包含 2 个字符。",
|
||||
"email.validation.message": "输入您的用户名或电子邮件",
|
||||
"password.validation.message": "未满足密码条件",
|
||||
"account.activation.success.message.title": "成功!您已激活您的帐户。",
|
||||
@@ -113,10 +106,11 @@
|
||||
"recommendation.page.title": "建议 | {siteName}",
|
||||
"recommendation.page.heading": "我们有一些建议可以帮助您入门。",
|
||||
"recommendation.skip.button": "暂时跳过",
|
||||
"recommendation.option.trending": "Trending",
|
||||
"recommendation.option.trending": "Trending Now",
|
||||
"recommendation.option.popular": "Most Popular",
|
||||
"recommendation.product-card.pill-text.course": "Course",
|
||||
"recommendation.product-card.pill-text.professional-certificate": "Professional Certificate",
|
||||
"recommendation.option.recommended.for.you": "Recommended For You",
|
||||
"recommendation.product-card.pill-text.course": "课程",
|
||||
"recommendation.product-card.pill-text.professional-certificate": "专业证书",
|
||||
"recommendation.product-card.pill-text.emeritus": "Offered on Emeritus",
|
||||
"recommendation.product-card.pill-text.shorelight": "Offered through Shorelight",
|
||||
"recommendation.product-card.footer-text.number-of-courses": "{length} {label}",
|
||||
@@ -160,9 +154,6 @@
|
||||
"terms.of.service": "服务条款",
|
||||
"registration.username.suggestion.label": "建议:",
|
||||
"did.you.mean.alert.text": "你的意思是",
|
||||
"register.page.terms.of.service.and.honor.code": "创建帐户即表示您同意 {tosAndHonorCode} 并承认 {platformName} 和每位会员根据 {privacyPolicy} 处理您的个人数据。",
|
||||
"register.page.honor.code": "我同意 {platformName} {tosAndHonorCode}",
|
||||
"register.page.terms.of.service": "我同意接受 {platformName} {termsOfService}",
|
||||
"sign.in": "登录",
|
||||
"reset.password.page.title": "重设密码 | {siteName}",
|
||||
"reset.password": "重置密码",
|
||||
@@ -176,5 +167,15 @@
|
||||
"reset.server.rate.limit.error": "请求过多。",
|
||||
"reset.password.success.heading": "密码重置完成。",
|
||||
"reset.password.success": "您的密码已重置。登录到您的帐户。",
|
||||
"rate.limit.error": "由于请求过多而发生错误。请稍后重试。"
|
||||
}
|
||||
"rate.limit.error": "由于请求过多而发生错误。请稍后重试。",
|
||||
"start.learning": "开始学习",
|
||||
"with.site.name": "{siteName}",
|
||||
"your.career.turning.point": "Your career turning point",
|
||||
"is.here": "is here.",
|
||||
"welcome.to.platform": "欢迎来到{siteName},{username}!",
|
||||
"complete.your.profile.1": "完成",
|
||||
"complete.your.profile.2": "个人资料",
|
||||
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each Member process your personal data in accordance with the {privacyPolicy}.",
|
||||
"register.page.honor.code": "I agree to the {platformName} {tosAndHonorCode}",
|
||||
"register.page.terms.of.service": "I agree to the {platformName} {termsOfService}"
|
||||
}
|
||||
|
||||
@@ -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 < 2) {
|
||||
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,15 +59,10 @@ 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.',
|
||||
description: 'Validation message that appears when username or email address is less than 3 characters',
|
||||
defaultMessage: 'Username or email must have at least 2 characters.',
|
||||
description: 'Validation message that appears when username or email address is less than 2 characters',
|
||||
},
|
||||
'email.validation.message': {
|
||||
id: 'email.validation.message',
|
||||
|
||||
@@ -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' };
|
||||
it('should match state for invalid email (less than 2 characters), on form submission', () => {
|
||||
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} />));
|
||||
|
||||
// Check that loginRequestFailure was dispatched and state is updated
|
||||
expect(loginPage.state('errors')).toEqual(errorState);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(loginRequestFailure({ errorCode: 'invalid-form' }));
|
||||
fireEvent.change(screen.getByText(
|
||||
'',
|
||||
{ selector: '#password' },
|
||||
), { target: { value: 'test' } });
|
||||
fireEvent.change(screen.getByText(
|
||||
'',
|
||||
{ selector: '#emailOrUsername' },
|
||||
), { target: { value: 't' } });
|
||||
|
||||
fireEvent.click(screen.getByText(
|
||||
'',
|
||||
{ selector: '.btn-brand' },
|
||||
));
|
||||
|
||||
expect(screen.getByText('Username or email must have at least 2 characters.')).toBeDefined();
|
||||
});
|
||||
|
||||
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);
|
||||
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' },
|
||||
));
|
||||
|
||||
const loginPage = (mount(reduxWrapper(<IntlLoginPage {...props} />))).find('LoginPage');
|
||||
expect(container.querySelector('div[feedback-for="emailOrUsername"]').textContent).toEqual(emptyFieldValidation.emailOrUsername);
|
||||
expect(container.querySelector('div[feedback-for="password"]').textContent).toEqual(emptyFieldValidation.password);
|
||||
|
||||
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');
|
||||
|
||||
expect(loginPage.state('errors')).toEqual(errorState);
|
||||
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 reset field related error messages on onFocus event', () => {
|
||||
const errorState = { emailOrUsername: '', password: '' };
|
||||
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: 't', 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 2 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';
|
||||
|
||||
@@ -39,6 +40,7 @@ const Logistration = (props) => {
|
||||
const [key, setKey] = useState('');
|
||||
const navigate = useNavigate();
|
||||
const disablePublicAccountCreation = getConfig().ALLOW_PUBLIC_ACCOUNT_CREATION === false;
|
||||
const hideRegistrationLink = getConfig().SHOW_REGISTRATION_LINKS === false;
|
||||
|
||||
useEffect(() => {
|
||||
const authService = getAuthService();
|
||||
@@ -64,11 +66,16 @@ const Logistration = (props) => {
|
||||
setInstitutionLogin(!institutionLogin);
|
||||
};
|
||||
|
||||
const handleOnSelect = (tabKey) => {
|
||||
const handleOnSelect = (tabKey, currentTab) => {
|
||||
if (tabKey === currentTab) {
|
||||
return;
|
||||
}
|
||||
sendTrackEvent(`edx.bi.${tabKey.replace('/', '')}_form.toggled`, { category: 'user-engagement' });
|
||||
props.clearThirdPartyAuthContextErrorMessage();
|
||||
if (tabKey === LOGIN_PAGE) {
|
||||
props.backupRegistrationForm();
|
||||
} else if (tabKey === REGISTER_PAGE) {
|
||||
props.backupLoginForm();
|
||||
}
|
||||
setKey(tabKey);
|
||||
};
|
||||
@@ -116,8 +123,8 @@ const Logistration = (props) => {
|
||||
<Tab title={tabTitle} eventKey={selectedPage === LOGIN_PAGE ? LOGIN_PAGE : REGISTER_PAGE} />
|
||||
</Tabs>
|
||||
)
|
||||
: (!isValidTpaHint() && (
|
||||
<Tabs defaultActiveKey={selectedPage} id="controlled-tab" onSelect={handleOnSelect}>
|
||||
: (!isValidTpaHint() && !hideRegistrationLink && (
|
||||
<Tabs defaultActiveKey={selectedPage} id="controlled-tab" onSelect={(tabKey) => handleOnSelect(tabKey, selectedPage)}>
|
||||
<Tab title={formatMessage(messages['logistration.register'])} eventKey={REGISTER_PAGE} />
|
||||
<Tab title={formatMessage(messages['logistration.sign.in'])} eventKey={LOGIN_PAGE} />
|
||||
</Tabs>
|
||||
@@ -126,6 +133,11 @@ const Logistration = (props) => {
|
||||
<Navigate to={updatePathWithQueryParams(key)} replace />
|
||||
)}
|
||||
<div id="main-content" className="main-content">
|
||||
{!institutionLogin && !isValidTpaHint() && hideRegistrationLink && (
|
||||
<h3 className="mb-4.5">
|
||||
{formatMessage(messages[selectedPage === LOGIN_PAGE ? 'logistration.sign.in' : 'logistration.register'])}
|
||||
</h3>
|
||||
)}
|
||||
{selectedPage === LOGIN_PAGE
|
||||
? <LoginPage institutionLogin={institutionLogin} handleInstitutionLogin={handleInstitutionLogin} />
|
||||
: (
|
||||
@@ -144,6 +156,7 @@ const Logistration = (props) => {
|
||||
|
||||
Logistration.propTypes = {
|
||||
selectedPage: PropTypes.string,
|
||||
backupLoginForm: PropTypes.func.isRequired,
|
||||
backupRegistrationForm: PropTypes.func.isRequired,
|
||||
clearThirdPartyAuthContextErrorMessage: PropTypes.func.isRequired,
|
||||
tpaProviders: PropTypes.shape({
|
||||
@@ -170,6 +183,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,
|
||||
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', () => ({
|
||||
@@ -94,27 +94,56 @@ describe('Logistration', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should do nothing when user clicks on the same tab (login/register) again', () => {
|
||||
const { container } = render(reduxWrapper(<IntlLogistration />));
|
||||
// While staying on the registration form, clicking the register tab again
|
||||
fireEvent.click(container.querySelector('a[data-rb-event-key="/register"]'));
|
||||
|
||||
expect(sendTrackEvent).not.toHaveBeenCalledWith('edx.bi.register_form.toggled', { category: 'user-engagement' });
|
||||
});
|
||||
|
||||
it('should render registration page', () => {
|
||||
mergeConfig({
|
||||
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', () => {
|
||||
mergeConfig({
|
||||
SHOW_REGISTRATION_LINKS: false,
|
||||
});
|
||||
|
||||
let props = { selectedPage: LOGIN_PAGE };
|
||||
const { rerender } = render(reduxWrapper(<IntlLogistration {...props} />));
|
||||
|
||||
// verifying sign in heading
|
||||
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 };
|
||||
rerender(reduxWrapper(<IntlLogistration {...props} />));
|
||||
|
||||
// verifying register heading
|
||||
expect(screen.getByRole('heading', { level: 3 }).textContent).toEqual('Register');
|
||||
});
|
||||
|
||||
it('should render only login page when public account creation is disabled', () => {
|
||||
mergeConfig({
|
||||
ALLOW_PUBLIC_ACCOUNT_CREATION: false,
|
||||
DISABLE_ENTERPRISE_LOGIN: 'true',
|
||||
SHOW_REGISTRATION_LINKS: 'true',
|
||||
});
|
||||
|
||||
store = mockStore({
|
||||
@@ -131,14 +160,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', () => {
|
||||
@@ -161,12 +190,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: '',
|
||||
@@ -192,8 +221,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');
|
||||
@@ -224,9 +253,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: '',
|
||||
@@ -235,15 +264,23 @@ 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 props = { selectedPage: LOGIN_PAGE };
|
||||
const { container } = render(reduxWrapper(<IntlLogistration {...props} />));
|
||||
fireEvent.click(container.querySelector('a[data-rb-event-key="/register"]'));
|
||||
expect(store.dispatch).toHaveBeenCalledWith(backupLoginForm());
|
||||
});
|
||||
|
||||
it('should clear tpa context errorMessage tab click', () => {
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
const 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());
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,9 +14,10 @@ import {
|
||||
Alert,
|
||||
Form,
|
||||
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';
|
||||
@@ -155,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 || '',
|
||||
},
|
||||
);
|
||||
@@ -194,7 +196,7 @@ const ProgressiveProfiling = (props) => {
|
||||
});
|
||||
|
||||
return (
|
||||
<BaseContainer showWelcomeBanner username={authenticatedUser?.username}>
|
||||
<BaseContainer showWelcomeBanner fullName={authenticatedUser?.fullName || authenticatedUser?.name}>
|
||||
<Helmet>
|
||||
<title>{formatMessage(messages['progressive.profiling.page.title'],
|
||||
{ siteName: getConfig().SITE_NAME })}
|
||||
@@ -217,57 +219,63 @@ const ProgressiveProfiling = (props) => {
|
||||
/>
|
||||
)}
|
||||
<div className="mw-xs m-4 pp-page-content">
|
||||
<div>
|
||||
<h2 className="pp-page__heading text-primary">{formatMessage(messages['progressive.profiling.page.heading'])}</h2>
|
||||
</div>
|
||||
<hr className="border-light-700 mb-4" />
|
||||
{showError ? (
|
||||
<Alert id="pp-page-errors" className="mb-3" variant="danger" icon={Error}>
|
||||
<Alert.Heading>{formatMessage(messages['welcome.page.error.heading'])}</Alert.Heading>
|
||||
<p>{formatMessage(messages['welcome.page.error.message'])}</p>
|
||||
</Alert>
|
||||
) : null}
|
||||
<Form>
|
||||
{formFields}
|
||||
{(getConfig().AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK) && (
|
||||
<span className="pp-page__support-link">
|
||||
<Hyperlink
|
||||
isInline
|
||||
variant="muted"
|
||||
destination={getConfig().AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK}
|
||||
target="_blank"
|
||||
showLaunchIcon={false}
|
||||
onClick={() => (sendTrackEvent('edx.bi.welcome.page.support.link.clicked'))}
|
||||
>
|
||||
{formatMessage(messages['optional.fields.information.link'])}
|
||||
</Hyperlink>
|
||||
</span>
|
||||
)}
|
||||
<div className="d-flex mt-4 mb-3">
|
||||
<StatefulButton
|
||||
type="submit"
|
||||
variant="brand"
|
||||
className="pp-page__button-width"
|
||||
state={submitState}
|
||||
labels={{
|
||||
default: showRecommendationsPage ? formatMessage(messages['optional.fields.next.button']) : formatMessage(messages['optional.fields.submit.button']),
|
||||
pending: '',
|
||||
}}
|
||||
onClick={handleSubmit}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
/>
|
||||
<StatefulButton
|
||||
className="text-gray-700 font-weight-500"
|
||||
type="submit"
|
||||
variant="link"
|
||||
labels={{
|
||||
default: formatMessage(messages['optional.fields.skip.button']),
|
||||
}}
|
||||
onClick={handleSkip}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
/>
|
||||
</div>
|
||||
</Form>
|
||||
{registrationEmbedded && welcomePageContextApiStatus === PENDING_STATE ? (
|
||||
<Spinner animation="border" variant="primary" id="tpa-spinner" />
|
||||
) : (
|
||||
<>
|
||||
<div>
|
||||
<h2 className="pp-page__heading text-primary">{formatMessage(messages['progressive.profiling.page.heading'])}</h2>
|
||||
</div><hr className="border-light-700 mb-4" />
|
||||
{showError ? (
|
||||
<Alert id="pp-page-errors" className="mb-3" variant="danger" icon={Error}>
|
||||
<Alert.Heading>{formatMessage(messages['welcome.page.error.heading'])}</Alert.Heading>
|
||||
<p>{formatMessage(messages['welcome.page.error.message'])}</p>
|
||||
</Alert>
|
||||
) : null}
|
||||
<Form>
|
||||
{formFields}
|
||||
{(getConfig().AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK) && (
|
||||
<span className="pp-page__support-link">
|
||||
<Hyperlink
|
||||
isInline
|
||||
variant="muted"
|
||||
destination={getConfig().AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK}
|
||||
target="_blank"
|
||||
showLaunchIcon={false}
|
||||
onClick={() => (sendTrackEvent('edx.bi.welcome.page.support.link.clicked'))}
|
||||
>
|
||||
{formatMessage(messages['optional.fields.information.link'])}
|
||||
</Hyperlink>
|
||||
</span>
|
||||
)}
|
||||
<div className="d-flex mt-4 mb-3">
|
||||
<StatefulButton
|
||||
type="submit"
|
||||
variant="brand"
|
||||
className="pp-page__button-width"
|
||||
state={submitState}
|
||||
labels={{
|
||||
default: showRecommendationsPage ? formatMessage(messages['optional.fields.next.button']) : formatMessage(messages['optional.fields.submit.button']),
|
||||
pending: '',
|
||||
}}
|
||||
onClick={handleSubmit}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
/>
|
||||
<StatefulButton
|
||||
className="text-gray-700 font-weight-500"
|
||||
type="submit"
|
||||
variant="link"
|
||||
labels={{
|
||||
default: formatMessage(messages['optional.fields.skip.button']),
|
||||
}}
|
||||
onClick={handleSkip}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
/>
|
||||
</div>
|
||||
</Form>
|
||||
</>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</BaseContainer>
|
||||
);
|
||||
@@ -277,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';
|
||||
|
||||
@@ -5,7 +5,9 @@ import { getConfig, mergeConfig } from '@edx/frontend-platform';
|
||||
import { identifyAuthenticatedUser, sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import { configure, injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { mount } from 'enzyme';
|
||||
import {
|
||||
fireEvent, render, screen,
|
||||
} from '@testing-library/react';
|
||||
import { MemoryRouter, mockNavigate, useLocation } from 'react-router-dom';
|
||||
import configureStore from 'redux-mock-store';
|
||||
|
||||
@@ -14,6 +16,7 @@ import {
|
||||
COMPLETE_STATE, DEFAULT_REDIRECT_URL,
|
||||
EMBEDDED,
|
||||
FAILURE_STATE,
|
||||
PENDING_STATE,
|
||||
RECOMMENDATIONS,
|
||||
} from '../../data/constants';
|
||||
import { saveUserProfile } from '../data/actions';
|
||||
@@ -111,34 +114,43 @@ describe('ProgressiveProfilingTests', () => {
|
||||
mergeConfig({
|
||||
AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK: '',
|
||||
});
|
||||
const progressiveProfilingPage = mount(reduxWrapper(<IntlProgressiveProfilingPage />));
|
||||
const { queryByRole } = render(reduxWrapper(<IntlProgressiveProfilingPage />));
|
||||
const button = queryByRole('button', { name: /learn more about how we use this information/i });
|
||||
|
||||
expect(progressiveProfilingPage.find('a.pgn__hyperlink').exists()).toBeFalsy();
|
||||
expect(button).toBeNull();
|
||||
});
|
||||
|
||||
it('should display button "Learn more about how we use this information."', () => {
|
||||
mergeConfig({
|
||||
AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK: 'http://localhost:1999/support',
|
||||
});
|
||||
const progressiveProfilingPage = mount(reduxWrapper(<IntlProgressiveProfilingPage />));
|
||||
|
||||
expect(progressiveProfilingPage.find('a.pgn__hyperlink').text()).toEqual('Learn more about how we use this information.');
|
||||
const { getByText } = render(reduxWrapper(<IntlProgressiveProfilingPage />));
|
||||
|
||||
const learnMoreButton = getByText('Learn more about how we use this information.');
|
||||
|
||||
expect(learnMoreButton).toBeDefined();
|
||||
});
|
||||
|
||||
it('should open modal on pressing skip for now button', () => {
|
||||
delete window.location;
|
||||
window.location = { href: getConfig().BASE_URL.concat(AUTHN_PROGRESSIVE_PROFILING) };
|
||||
const progressiveProfilingPage = mount(reduxWrapper(<IntlProgressiveProfilingPage />));
|
||||
const { getByRole } = render(reduxWrapper(<IntlProgressiveProfilingPage />));
|
||||
|
||||
const skipButton = getByRole('button', { name: /skip for now/i });
|
||||
fireEvent.click(skipButton);
|
||||
|
||||
const modalContentContainer = document.getElementsByClassName('.pgn__modal-content-container');
|
||||
|
||||
expect(modalContentContainer).toBeTruthy();
|
||||
|
||||
progressiveProfilingPage.find('button.btn-link').simulate('click');
|
||||
expect(progressiveProfilingPage.find('.pgn__modal-content-container').exists()).toBeTruthy();
|
||||
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.welcome.page.skip.link.clicked', { host: '' });
|
||||
});
|
||||
|
||||
// ******** test event functionality ********
|
||||
|
||||
it('should make identify call to segment on progressive profiling page', () => {
|
||||
mount(reduxWrapper(<IntlProgressiveProfilingPage />));
|
||||
render(reduxWrapper(<IntlProgressiveProfilingPage />));
|
||||
|
||||
expect(identifyAuthenticatedUser).toHaveBeenCalledWith(3);
|
||||
expect(identifyAuthenticatedUser).toHaveBeenCalled();
|
||||
@@ -148,9 +160,11 @@ describe('ProgressiveProfilingTests', () => {
|
||||
mergeConfig({
|
||||
AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK: 'http://localhost:1999/support',
|
||||
});
|
||||
const progressiveProfilingPage = mount(reduxWrapper(<IntlProgressiveProfilingPage />));
|
||||
render(reduxWrapper(<IntlProgressiveProfilingPage />));
|
||||
|
||||
const supportLink = screen.getByRole('link', { name: /learn more about how we use this information/i });
|
||||
fireEvent.click(supportLink);
|
||||
|
||||
progressiveProfilingPage.find('.pp-page__support-link a[target="_blank"]').simulate('click');
|
||||
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.welcome.page.support.link.clicked');
|
||||
});
|
||||
|
||||
@@ -159,13 +173,16 @@ describe('ProgressiveProfilingTests', () => {
|
||||
isGenderSelected: false,
|
||||
isYearOfBirthSelected: false,
|
||||
isLevelOfEducationSelected: false,
|
||||
isWorkExperienceSelected: false,
|
||||
host: '',
|
||||
};
|
||||
delete window.location;
|
||||
window.location = { href: getConfig().BASE_URL.concat(AUTHN_PROGRESSIVE_PROFILING) };
|
||||
const progressiveProfilingPage = mount(reduxWrapper(<IntlProgressiveProfilingPage />));
|
||||
render(reduxWrapper(<IntlProgressiveProfilingPage />));
|
||||
|
||||
const nextButton = screen.getByText('Next');
|
||||
fireEvent.click(nextButton);
|
||||
|
||||
progressiveProfilingPage.find('button.btn-brand').simulate('click');
|
||||
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.welcome.page.submit.clicked', expectedEventProperties);
|
||||
});
|
||||
|
||||
@@ -177,12 +194,16 @@ describe('ProgressiveProfilingTests', () => {
|
||||
extended_profile: [{ field_name: 'company', field_value: 'test company' }],
|
||||
};
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
const progressiveProfilingPage = mount(reduxWrapper(<IntlProgressiveProfilingPage />));
|
||||
const { getByLabelText, getByText } = render(reduxWrapper(<IntlProgressiveProfilingPage />));
|
||||
|
||||
progressiveProfilingPage.find('select#gender').simulate('change', { target: { value: 'm', name: 'gender' } });
|
||||
progressiveProfilingPage.find('input#company').simulate('change', { target: { value: 'test company', name: 'company' } });
|
||||
const genderSelect = getByLabelText('Gender');
|
||||
const companyInput = getByLabelText('Company');
|
||||
|
||||
fireEvent.change(genderSelect, { target: { value: 'm' } });
|
||||
fireEvent.change(companyInput, { target: { value: 'test company' } });
|
||||
|
||||
fireEvent.click(getByText('Next'));
|
||||
|
||||
progressiveProfilingPage.find('button.btn-brand').simulate('click');
|
||||
expect(store.dispatch).toHaveBeenCalledWith(saveUserProfile('abc123', formPayload));
|
||||
});
|
||||
|
||||
@@ -195,8 +216,10 @@ describe('ProgressiveProfilingTests', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const progressiveProfilingPage = mount(reduxWrapper(<IntlProgressiveProfilingPage />));
|
||||
expect(progressiveProfilingPage.find('#pp-page-errors').exists()).toBeTruthy();
|
||||
const { container } = render(reduxWrapper(<IntlProgressiveProfilingPage />));
|
||||
const errorElement = container.querySelector('#pp-page-errors');
|
||||
|
||||
expect(errorElement).toBeTruthy();
|
||||
});
|
||||
|
||||
// ******** miscellaneous tests ********
|
||||
@@ -209,7 +232,7 @@ describe('ProgressiveProfilingTests', () => {
|
||||
href: getConfig().BASE_URL,
|
||||
};
|
||||
|
||||
mount(reduxWrapper(<IntlProgressiveProfilingPage />));
|
||||
render(reduxWrapper(<IntlProgressiveProfilingPage />));
|
||||
expect(window.location.href).toEqual(DASHBOARD_URL);
|
||||
});
|
||||
|
||||
@@ -227,9 +250,10 @@ describe('ProgressiveProfilingTests', () => {
|
||||
success: true,
|
||||
},
|
||||
});
|
||||
const progressiveProfilingPage = mount(reduxWrapper(<IntlProgressiveProfilingPage />));
|
||||
const { container } = render(reduxWrapper(<IntlProgressiveProfilingPage />));
|
||||
const nextButton = container.querySelector('button.btn-brand');
|
||||
expect(nextButton.textContent).toEqual('Next');
|
||||
|
||||
expect(progressiveProfilingPage.find('button.btn-brand').text()).toEqual('Next');
|
||||
expect(mockNavigate).toHaveBeenCalledWith(RECOMMENDATIONS);
|
||||
});
|
||||
|
||||
@@ -253,9 +277,10 @@ describe('ProgressiveProfilingTests', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const progressiveProfilingPage = mount(reduxWrapper(<IntlProgressiveProfilingPage />));
|
||||
const { container } = render(reduxWrapper(<IntlProgressiveProfilingPage />));
|
||||
const nextButton = container.querySelector('button.btn-brand');
|
||||
expect(nextButton.textContent).toEqual('Submit');
|
||||
|
||||
expect(progressiveProfilingPage.find('button.btn-brand').text()).toEqual('Submit');
|
||||
expect(window.location.href).toEqual(redirectUrl);
|
||||
});
|
||||
});
|
||||
@@ -286,17 +311,43 @@ describe('ProgressiveProfilingTests', () => {
|
||||
href: getConfig().BASE_URL.concat(AUTHN_PROGRESSIVE_PROFILING),
|
||||
search: `?host=${host}&variant=${EMBEDDED}`,
|
||||
};
|
||||
const progressiveProfilingPage = mount(reduxWrapper(<IntlProgressiveProfilingPage />));
|
||||
render(reduxWrapper(<IntlProgressiveProfilingPage />));
|
||||
|
||||
const skipLinkButton = screen.getByText('Skip for now');
|
||||
fireEvent.click(skipLinkButton);
|
||||
|
||||
progressiveProfilingPage.find('button.btn-link').simulate('click');
|
||||
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.welcome.page.skip.link.clicked', { host });
|
||||
});
|
||||
|
||||
it('should show spinner while fetching the optional fields', () => {
|
||||
delete window.location;
|
||||
window.location = {
|
||||
assign: jest.fn().mockImplementation((value) => { window.location.href = value; }),
|
||||
href: getConfig().BASE_URL.concat(AUTHN_PROGRESSIVE_PROFILING),
|
||||
search: `?host=${host}&variant=${EMBEDDED}`,
|
||||
};
|
||||
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
commonComponents: {
|
||||
...initialState.commonComponents,
|
||||
thirdPartyAuthApiStatus: PENDING_STATE,
|
||||
optionalFields,
|
||||
},
|
||||
});
|
||||
|
||||
const { container } = render(reduxWrapper(<IntlProgressiveProfilingPage />));
|
||||
|
||||
const tpaSpinnerElement = container.querySelector('#tpa-spinner');
|
||||
expect(tpaSpinnerElement).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should set host property value to host where iframe is embedded for on ramp experience', () => {
|
||||
const expectedEventProperties = {
|
||||
isGenderSelected: false,
|
||||
isYearOfBirthSelected: false,
|
||||
isLevelOfEducationSelected: false,
|
||||
isWorkExperienceSelected: false,
|
||||
host: 'http://example.com',
|
||||
};
|
||||
delete window.location;
|
||||
@@ -304,8 +355,10 @@ describe('ProgressiveProfilingTests', () => {
|
||||
href: getConfig().BASE_URL.concat(AUTHN_PROGRESSIVE_PROFILING),
|
||||
search: `?host=${host}`,
|
||||
};
|
||||
const progressiveProfilingPage = mount(reduxWrapper(<IntlProgressiveProfilingPage />));
|
||||
progressiveProfilingPage.find('button.btn-brand').simulate('click');
|
||||
render(reduxWrapper(<IntlProgressiveProfilingPage />));
|
||||
const submitButton = screen.getByText('Next');
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.welcome.page.submit.clicked', expectedEventProperties);
|
||||
});
|
||||
|
||||
@@ -317,8 +370,10 @@ describe('ProgressiveProfilingTests', () => {
|
||||
search: `?variant=${EMBEDDED}&host=${host}`,
|
||||
};
|
||||
|
||||
const progressiveProfilingPage = mount(reduxWrapper(<IntlProgressiveProfilingPage />));
|
||||
expect(progressiveProfilingPage.find('#gender').exists()).toBeTruthy();
|
||||
const { container } = render(reduxWrapper(<IntlProgressiveProfilingPage />));
|
||||
|
||||
const genderField = container.querySelector('#gender');
|
||||
expect(genderField).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should redirect to dashboard if API call to get form field fails', () => {
|
||||
@@ -336,7 +391,7 @@ describe('ProgressiveProfilingTests', () => {
|
||||
},
|
||||
});
|
||||
|
||||
mount(reduxWrapper(<IntlProgressiveProfilingPage />));
|
||||
render(reduxWrapper(<IntlProgressiveProfilingPage />));
|
||||
expect(window.location.href).toBe(DASHBOARD_URL);
|
||||
});
|
||||
|
||||
@@ -364,8 +419,9 @@ describe('ProgressiveProfilingTests', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const progressiveProfilingPage = mount(reduxWrapper(<IntlProgressiveProfilingPage />));
|
||||
progressiveProfilingPage.find('button.btn-brand').simulate('click');
|
||||
render(reduxWrapper(<IntlProgressiveProfilingPage />));
|
||||
const submitButton = screen.getByText('Submit');
|
||||
fireEvent.click(submitButton);
|
||||
expect(window.location.href).toBe(redirectUrl);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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';
|
||||
@@ -20,7 +20,7 @@ const BaseCard = ({
|
||||
<div className="recommendation-card" key={`container-${uuid}`}>
|
||||
<Hyperlink
|
||||
target="_blank"
|
||||
className="card-box"
|
||||
className="card-box d-inline"
|
||||
showLaunchIcon={false}
|
||||
onClick={handleOnClick}
|
||||
>
|
||||
@@ -39,8 +39,8 @@ const BaseCard = ({
|
||||
/>
|
||||
<Card.Header
|
||||
className="mt-2"
|
||||
title={truncateText(title)}
|
||||
subtitle={truncateText(subtitle)}
|
||||
title={truncateText(title, 50)}
|
||||
subtitle={truncateText(subtitle, 30)}
|
||||
/>
|
||||
<Card.Section className="d-flex">
|
||||
<div className="product-badge">
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { mount } from 'enzyme';
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
import SmallLayout from './SmallLayout';
|
||||
import mockedRecommendedProducts from '../data/tests/mockedData';
|
||||
@@ -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(),
|
||||
}));
|
||||
|
||||
@@ -36,8 +36,11 @@ describe('RecommendationsPageTests', () => {
|
||||
});
|
||||
|
||||
it('should render recommendations when recommendations are not loading', () => {
|
||||
const recommendationsPage = mount(reduxWrapper(<IntlRecommendationsSmallLayoutPage {...props} />));
|
||||
expect(recommendationsPage.find('.react-loading-skeleton').exists()).toBeFalsy();
|
||||
const { container } = render(reduxWrapper(<IntlRecommendationsSmallLayoutPage {...props} />));
|
||||
|
||||
const reactLoadingSkeleton = container.querySelector('.react-loading-skeleton');
|
||||
|
||||
expect(reactLoadingSkeleton).toBeNull();
|
||||
});
|
||||
|
||||
it('should render loading state when recommendations are loading', () => {
|
||||
@@ -45,7 +48,10 @@ describe('RecommendationsPageTests', () => {
|
||||
...props,
|
||||
isLoading: true,
|
||||
};
|
||||
const recommendationsPage = mount(reduxWrapper(<IntlRecommendationsSmallLayoutPage {...props} />));
|
||||
expect(recommendationsPage.find('.react-loading-skeleton').exists()).toBeTruthy();
|
||||
const { container } = render(reduxWrapper(<IntlRecommendationsSmallLayoutPage {...props} />));
|
||||
|
||||
const reactLoadingSkeleton = container.querySelector('.react-loading-skeleton');
|
||||
|
||||
expect(reactLoadingSkeleton).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -47,7 +47,9 @@ export const getVariant = (productType) => (
|
||||
|
||||
export const createCodeFriendlyProduct = (type) => type?.replace(/\s+/g, '-').replace(/'/g, '').toLowerCase();
|
||||
|
||||
export const truncateText = (input) => (input?.length > 50 ? `${input.substring(0, 50)}...` : input);
|
||||
export const truncateText = (input, textLength) => (
|
||||
input?.length > textLength ? `${input.substring(0, textLength)}...` : input
|
||||
);
|
||||
|
||||
export const filterLocationRestriction = (products, countryCode) => products.filter((product) => {
|
||||
if (product.locationRestriction) {
|
||||
|
||||
@@ -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 { render } from '@testing-library/react';
|
||||
import configureStore from 'redux-mock-store';
|
||||
|
||||
import mockedProductData from './mockedData';
|
||||
@@ -25,8 +25,10 @@ describe('RecommendationsListTests', () => {
|
||||
userId: 1234567,
|
||||
};
|
||||
|
||||
const recommendationsList = mount(reduxWrapper(<IntlRecommendationList {...props} />));
|
||||
expect(recommendationsList.find('.recommendation-card').length).toEqual(mockedProductData.length);
|
||||
const { container } = render(reduxWrapper(<IntlRecommendationList {...props} />));
|
||||
|
||||
const recommendationCards = container.querySelectorAll('.recommendation-card');
|
||||
expect(recommendationCards.length).toEqual(mockedProductData.length);
|
||||
});
|
||||
|
||||
it('should render the recommendations card with footer content', () => {
|
||||
@@ -35,8 +37,12 @@ describe('RecommendationsListTests', () => {
|
||||
userId: 1234567,
|
||||
};
|
||||
|
||||
const recommendationsList = mount(reduxWrapper(<IntlRecommendationList {...props} />));
|
||||
expect(recommendationsList.find('.x-small').at(0).text()).toEqual('1 Course');
|
||||
expect(recommendationsList.find('.x-small').at(1).text()).toEqual('2 Courses');
|
||||
const { getByText } = render(reduxWrapper(<IntlRecommendationList {...props} />));
|
||||
|
||||
const firstFooterContent = getByText('1 Course');
|
||||
const secondFooterContent = getByText('2 Courses');
|
||||
|
||||
expect(firstFooterContent).toBeTruthy();
|
||||
expect(secondFooterContent).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,8 +4,8 @@ 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 { mount } from 'enzyme';
|
||||
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(),
|
||||
}));
|
||||
|
||||
@@ -77,7 +77,7 @@ describe('RecommendationsPageTests', () => {
|
||||
});
|
||||
|
||||
it('should redirect to dashboard if user is not coming from registration workflow', () => {
|
||||
mount(reduxWrapper(<IntlRecommendationsPage />));
|
||||
render(reduxWrapper(<IntlRecommendationsPage />));
|
||||
expect(window.location.href).toEqual(dashboardUrl);
|
||||
});
|
||||
|
||||
@@ -86,15 +86,16 @@ describe('RecommendationsPageTests', () => {
|
||||
recommendations: [],
|
||||
isLoading: false,
|
||||
});
|
||||
mount(reduxWrapper(<IntlRecommendationsPage />));
|
||||
render(reduxWrapper(<IntlRecommendationsPage />));
|
||||
expect(window.location.href).toEqual(dashboardUrl);
|
||||
});
|
||||
|
||||
it('should redirect user if they click "Skip for now" button', () => {
|
||||
mockUseLocation();
|
||||
jest.useFakeTimers();
|
||||
const recommendationsPage = mount(reduxWrapper(<IntlRecommendationsPage />));
|
||||
recommendationsPage.find('.pgn__stateful-btn-state-default').first().simulate('click');
|
||||
const { container } = render(reduxWrapper(<IntlRecommendationsPage />));
|
||||
const skipButton = container.querySelector('.pgn__stateful-btn-state-default');
|
||||
fireEvent.click(skipButton);
|
||||
jest.advanceTimersByTime(300);
|
||||
expect(window.location.href).toEqual(redirectUrl);
|
||||
});
|
||||
@@ -102,19 +103,25 @@ describe('RecommendationsPageTests', () => {
|
||||
it('should display recommendations small layout for small screen', () => {
|
||||
mockUseLocation();
|
||||
useMediaQuery.mockReturnValue(true);
|
||||
const recommendationsPage = mount(reduxWrapper(<IntlRecommendationsPage />));
|
||||
const { container } = render(reduxWrapper(<IntlRecommendationsPage />));
|
||||
|
||||
expect(recommendationsPage.find('#recommendations-small-layout').exists()).toBeTruthy();
|
||||
expect(recommendationsPage.find('.react-loading-skeleton').exists()).toBeFalsy();
|
||||
const recommendationsSmallLayout = container.querySelector('#recommendations-small-layout');
|
||||
const reactLoadingSkeleton = container.querySelector('.react-loading-skeleton');
|
||||
|
||||
expect(recommendationsSmallLayout).toBeTruthy();
|
||||
expect(reactLoadingSkeleton).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should display recommendations large layout for large screen', () => {
|
||||
mockUseLocation();
|
||||
useMediaQuery.mockReturnValue(false);
|
||||
const recommendationsPage = mount(reduxWrapper(<IntlRecommendationsPage />));
|
||||
const { container } = render(reduxWrapper(<IntlRecommendationsPage />));
|
||||
|
||||
expect(recommendationsPage.find('.pgn_collapsible').exists()).toBeFalsy();
|
||||
expect(recommendationsPage.find('.react-loading-skeleton').exists()).toBeFalsy();
|
||||
const pgnCollapsible = container.querySelector('.pgn_collapsible');
|
||||
const reactLoadingSkeleton = container.querySelector('.react-loading-skeleton');
|
||||
|
||||
expect(pgnCollapsible).toBeFalsy();
|
||||
expect(reactLoadingSkeleton).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should display skeletons if recommendations are loading for large screen', () => {
|
||||
@@ -124,9 +131,11 @@ describe('RecommendationsPageTests', () => {
|
||||
recommendations: [],
|
||||
isLoading: true,
|
||||
});
|
||||
const recommendationsPage = mount(reduxWrapper(<IntlRecommendationsPage />));
|
||||
const { container } = render(reduxWrapper(<IntlRecommendationsPage />));
|
||||
|
||||
expect(recommendationsPage.find('.react-loading-skeleton').exists()).toBeTruthy();
|
||||
const reactLoadingSkeleton = container.querySelector('.react-loading-skeleton');
|
||||
|
||||
expect(reactLoadingSkeleton).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should display skeletons if recommendations are loading for small screen', () => {
|
||||
@@ -136,9 +145,11 @@ describe('RecommendationsPageTests', () => {
|
||||
recommendations: [],
|
||||
isLoading: true,
|
||||
});
|
||||
const recommendationsPage = mount(reduxWrapper(<IntlRecommendationsPage />));
|
||||
const { container } = render(reduxWrapper(<IntlRecommendationsPage />));
|
||||
|
||||
expect(recommendationsPage.find('.react-loading-skeleton').exists()).toBeTruthy();
|
||||
const reactLoadingSkeleton = container.querySelector('.react-loading-skeleton');
|
||||
|
||||
expect(reactLoadingSkeleton).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should fire recommendations viewed event', () => {
|
||||
@@ -149,7 +160,7 @@ describe('RecommendationsPageTests', () => {
|
||||
});
|
||||
|
||||
useMediaQuery.mockReturnValue(false);
|
||||
mount(reduxWrapper(<IntlRecommendationsPage />));
|
||||
render(reduxWrapper(<IntlRecommendationsPage />));
|
||||
|
||||
expect(sendTrackEvent).toBeCalled();
|
||||
expect(sendTrackEvent).toHaveBeenCalledWith(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user