diff --git a/src/register/RegistrationFields/EmailField/EmailField.jsx b/src/register/RegistrationFields/EmailField/EmailField.jsx index 819414b58a..6521f6d727 100644 --- a/src/register/RegistrationFields/EmailField/EmailField.jsx +++ b/src/register/RegistrationFields/EmailField/EmailField.jsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useIntl } from '@edx/frontend-platform/i18n'; @@ -9,9 +9,9 @@ import PropTypes from 'prop-types'; import validateEmail from './validator'; import { FormGroup } from '../../../common-components'; import { - backupRegistrationFormBegin, clearRegistrationBackendError, fetchRealtimeValidations, + setEmailSuggestionInStore, } from '../../data/actions'; import messages from '../../messages'; @@ -44,6 +44,10 @@ const EmailField = (props) => { const [emailSuggestion, setEmailSuggestion] = useState({ ...backedUpFormData?.emailSuggestion }); + useEffect(() => { + setEmailSuggestion(backedUpFormData.emailSuggestion); + }, [backedUpFormData.emailSuggestion]); + const handleOnBlur = (e) => { const { value } = e.target; const { fieldError, confirmEmailError, suggestion } = validateEmail(value, confirmEmailValue, formatMessage); @@ -52,10 +56,7 @@ const EmailField = (props) => { handleErrorChange('confirm_email', confirmEmailError); } - dispatch(backupRegistrationFormBegin({ - ...backedUpFormData, - emailSuggestion: { ...suggestion }, - })); + dispatch(setEmailSuggestionInStore(suggestion)); setEmailSuggestion(suggestion); if (fieldError) { diff --git a/src/register/RegistrationFields/EmailField/EmailField.test.jsx b/src/register/RegistrationFields/EmailField/EmailField.test.jsx index 43825ca9d2..52d96ed9cd 100644 --- a/src/register/RegistrationFields/EmailField/EmailField.test.jsx +++ b/src/register/RegistrationFields/EmailField/EmailField.test.jsx @@ -46,7 +46,14 @@ describe('EmailField', () => { ); const initialState = { - register: {}, + register: { + registrationFormData: { + emailSuggestion: { + suggestion: 'example@gmail.com', + type: 'warning', + }, + }, + }, }; beforeEach(() => { diff --git a/src/register/RegistrationFields/EmailField/validator.js b/src/register/RegistrationFields/EmailField/validator.js index c59b9e67a1..44fd2fa1c8 100644 --- a/src/register/RegistrationFields/EmailField/validator.js +++ b/src/register/RegistrationFields/EmailField/validator.js @@ -91,7 +91,7 @@ export const validateEmailAddress = (value, username, domainName) => { const validateEmail = (value, confirmEmailValue, formatMessage) => { let fieldError = ''; let confirmEmailError = ''; - let emailSuggestion = {}; + let emailSuggestion = { suggestion: '', type: '' }; if (!value) { fieldError = formatMessage(messages['empty.email.field.error']); diff --git a/src/register/RegistrationFields/NameField/validator.js b/src/register/RegistrationFields/NameField/validator.js index e62c227d08..aefaedfb3f 100644 --- a/src/register/RegistrationFields/NameField/validator.js +++ b/src/register/RegistrationFields/NameField/validator.js @@ -10,7 +10,7 @@ export const HTML_REGEX = /<|>/u; export const INVALID_NAME_REGEX = /https?:\/\/(?:[-\w.]|(?:%[\da-fA-F]{2}))*/g; const validateName = (value, formatMessage) => { - let fieldError; + let fieldError = ''; if (!value.trim()) { fieldError = formatMessage(messages['empty.name.field.error']); } else if (URL_REGEX.test(value) || HTML_REGEX.test(value) || INVALID_NAME_REGEX.test(value)) { diff --git a/src/register/RegistrationPage.jsx b/src/register/RegistrationPage.jsx index f56106ff93..3a9426c2e5 100644 --- a/src/register/RegistrationPage.jsx +++ b/src/register/RegistrationPage.jsx @@ -18,6 +18,7 @@ import { backupRegistrationFormBegin, clearRegistrationBackendError, registerNewUser, + setEmailSuggestionInStore, setUserPipelineDataLoaded, } from './data/actions'; import { @@ -190,8 +191,8 @@ const RegistrationPage = (props) => { const value = event.target.type === 'checkbox' ? event.target.checked : event.target.value; if (registrationError[name]) { dispatch(clearRegistrationBackendError(name)); - setErrors(prevErrors => ({ ...prevErrors, [name]: '' })); } + setErrors(prevErrors => ({ ...prevErrors, [name]: '' })); setFormFields(prevState => ({ ...prevState, [name]: value })); }; @@ -225,7 +226,7 @@ const RegistrationPage = (props) => { } // Validating form data before submitting - const { isValid, fieldErrors } = isFormValid( + const { isValid, fieldErrors, emailSuggestion } = isFormValid( payload, registrationEmbedded ? temporaryErrors : errors, configurableFormFields, @@ -233,6 +234,7 @@ const RegistrationPage = (props) => { formatMessage, ); setErrors({ ...fieldErrors }); + dispatch(setEmailSuggestionInStore(emailSuggestion)); // returning if not valid if (!isValid) { diff --git a/src/register/RegistrationPage.test.jsx b/src/register/RegistrationPage.test.jsx index ba0ce4d4e6..2634713f63 100644 --- a/src/register/RegistrationPage.test.jsx +++ b/src/register/RegistrationPage.test.jsx @@ -221,6 +221,54 @@ describe('RegistrationPage', () => { expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...formPayload, country: 'PK' })); }); + it('should display an error when form is submitted with an invalid email', () => { + jest.spyOn(global.Date, 'now').mockImplementation(() => 0); + const emailError = "We couldn't create your account.Please check your responses and try again."; + + const formPayload = { + name: 'Petro', + username: 'petro_qa', + email: 'petro @example.com', + password: 'password1', + country: 'Ukraine', + honor_code: true, + totalRegistrationTime: 0, + }; + + store.dispatch = jest.fn(store.dispatch); + const { getByLabelText, container } = render(routerWrapper(reduxWrapper())); + populateRequiredFields(getByLabelText, formPayload, true); + + const button = container.querySelector('button.btn-brand'); + fireEvent.click(button); + + const validationErrors = container.querySelector('#validation-errors'); + expect(validationErrors.textContent).toContain(emailError); + }); + + it('should display an error when form is submitted with an invalid username', () => { + jest.spyOn(global.Date, 'now').mockImplementation(() => 0); + const usernameError = "We couldn't create your account.Please check your responses and try again."; + + const formPayload = { + name: 'Petro', + username: 'petro qa', + email: 'petro@example.com', + password: 'password1', + country: 'Ukraine', + honor_code: true, + totalRegistrationTime: 0, + }; + + store.dispatch = jest.fn(store.dispatch); + const { getByLabelText, container } = render(routerWrapper(reduxWrapper())); + populateRequiredFields(getByLabelText, formPayload, true); + const button = container.querySelector('button.btn-brand'); + fireEvent.click(button); + const validationErrors = container.querySelector('#validation-errors'); + expect(validationErrors.textContent).toContain(usernameError); + }); + it('should submit form with marketing email opt in value', () => { mergeConfig({ MARKETING_EMAILS_OPT_IN: 'true', diff --git a/src/register/components/tests/ConfigurableRegistrationForm.test.jsx b/src/register/components/tests/ConfigurableRegistrationForm.test.jsx index 88d3e39f97..537d11eb7e 100644 --- a/src/register/components/tests/ConfigurableRegistrationForm.test.jsx +++ b/src/register/components/tests/ConfigurableRegistrationForm.test.jsx @@ -348,6 +348,49 @@ describe('ConfigurableRegistrationForm', () => { expect(confirmEmailErrorElement.textContent).toEqual('The email addresses do not match.'); }); + it('should show error if email and confirm email fields do not match on submit click', () => { + const formPayload = { + name: 'Petro', + username: 'petro_qa', + email: 'petro@example.com', + password: 'password1', + country: 'Ukraine', + honor_code: true, + totalRegistrationTime: 0, + }; + + store = mockStore({ + ...initialState, + commonComponents: { + ...initialState.commonComponents, + fieldDescriptions: { + confirm_email: { + name: 'confirm_email', type: 'text', label: 'Confirm Email', + }, + country: { name: 'country' }, + }, + }, + }); + const { getByLabelText, container } = render(routerWrapper(reduxWrapper())); + + populateRequiredFields(getByLabelText, formPayload, true); + fireEvent.change( + getByLabelText('Confirm Email'), + { target: { value: 'test2@gmail.com', name: 'confirm_email' } }, + ); + + const button = container.querySelector('button.btn-brand'); + fireEvent.click(button); + + const confirmEmailErrorElement = container.querySelector('div#confirm_email-error'); + expect(confirmEmailErrorElement.textContent).toEqual('The email addresses do not match.'); + + const validationErrors = container.querySelector('#validation-errors'); + expect(validationErrors.textContent).toContain( + "We couldn't create your account.Please check your responses and try again.", + ); + }); + it('should run validations for configurable focused field on form submission', () => { const professionError = 'Enter your profession'; store = mockStore({ diff --git a/src/register/data/actions.js b/src/register/data/actions.js index d1316ed783..9fa5aed500 100644 --- a/src/register/data/actions.js +++ b/src/register/data/actions.js @@ -7,6 +7,7 @@ export const REGISTER_CLEAR_USERNAME_SUGGESTIONS = 'REGISTRATION_CLEAR_USERNAME_ export const REGISTRATION_CLEAR_BACKEND_ERROR = 'REGISTRATION_CLEAR_BACKEND_ERROR'; export const REGISTER_SET_COUNTRY_CODE = 'REGISTER_SET_COUNTRY_CODE'; export const REGISTER_SET_USER_PIPELINE_DATA_LOADED = 'REGISTER_SET_USER_PIPELINE_DATA_LOADED'; +export const REGISTER_SET_EMAIL_SUGGESTIONS = 'REGISTER_SET_EMAIL_SUGGESTIONS'; // Backup registration form export const backupRegistrationForm = () => ({ @@ -37,6 +38,12 @@ export const fetchRealtimeValidationsFailure = () => ({ type: REGISTER_FORM_VALIDATIONS.FAILURE, }); +// Set email field frontend validations +export const setEmailSuggestionInStore = (emailSuggestion) => ({ + type: REGISTER_SET_EMAIL_SUGGESTIONS, + payload: { emailSuggestion }, +}); + // Register export const registerNewUser = registrationInfo => ({ type: REGISTER_NEW_USER.BASE, diff --git a/src/register/data/reducers.js b/src/register/data/reducers.js index 0c9701b219..70c3a994d0 100644 --- a/src/register/data/reducers.js +++ b/src/register/data/reducers.js @@ -4,6 +4,7 @@ import { REGISTER_FORM_VALIDATIONS, REGISTER_NEW_USER, REGISTER_SET_COUNTRY_CODE, + REGISTER_SET_EMAIL_SUGGESTIONS, REGISTER_SET_USER_PIPELINE_DATA_LOADED, REGISTRATION_CLEAR_BACKEND_ERROR, } from './actions'; @@ -120,6 +121,14 @@ const reducer = (state = defaultState, action = {}) => { userPipelineDataLoaded: value, }; } + case REGISTER_SET_EMAIL_SUGGESTIONS: + return { + ...state, + registrationFormData: { + ...state.registrationFormData, + emailSuggestion: action.payload.emailSuggestion, + }, + }; default: return { ...state, diff --git a/src/register/data/tests/reducers.test.js b/src/register/data/tests/reducers.test.js index 07badb9cda..3e2270ab93 100644 --- a/src/register/data/tests/reducers.test.js +++ b/src/register/data/tests/reducers.test.js @@ -7,6 +7,7 @@ import { REGISTER_FORM_VALIDATIONS, REGISTER_NEW_USER, REGISTER_SET_COUNTRY_CODE, + REGISTER_SET_EMAIL_SUGGESTIONS, REGISTER_SET_USER_PIPELINE_DATA_LOADED, REGISTRATION_CLEAR_BACKEND_ERROR, } from '../actions'; @@ -65,6 +66,28 @@ describe('Registration Reducer Tests', () => { ); }); + it('should set email suggestions', () => { + const emailSuggestion = { + type: 'test type', + suggestion: 'test suggestion', + }; + const action = { + type: REGISTER_SET_EMAIL_SUGGESTIONS, + payload: { emailSuggestion }, + }; + + expect(reducer(defaultState, action)).toEqual( + { + ...defaultState, + registrationFormData: { + ...defaultState.registrationFormData, + emailSuggestion: { + type: 'test type', suggestion: 'test suggestion', + }, + }, + }); + }); + it('should set redirect url dashboard on registration success action', () => { const payload = { redirectUrl: `${getConfig().BASE_URL}${DEFAULT_REDIRECT_URL}`, diff --git a/src/register/data/utils.js b/src/register/data/utils.js index e1d0db7f75..b044f93dce 100644 --- a/src/register/data/utils.js +++ b/src/register/data/utils.js @@ -2,6 +2,9 @@ import { snakeCaseObject } from '@edx/frontend-platform'; import { LETTER_REGEX, NUMBER_REGEX } from '../../data/constants'; import messages from '../messages'; +import validateEmail from '../RegistrationFields/EmailField/validator'; +import validateName from '../RegistrationFields/NameField/validator'; +import validateUsername from '../RegistrationFields/UsernameField/validator'; /** * It validates the password field value @@ -35,12 +38,39 @@ export const isFormValid = ( ) => { const fieldErrors = { ...errors }; let isValid = true; + let emailSuggestion = { suggestion: '', type: '' }; + Object.keys(payload).forEach(key => { - if (!payload[key]) { - fieldErrors[key] = formatMessage(messages[`empty.${key}.field.error`]); + switch (key) { + case 'name': + fieldErrors.name = validateName(payload.name, formatMessage); + if (fieldErrors.name) { isValid = false; } + break; + case 'email': { + const { + fieldError, confirmEmailError, suggestion, + } = validateEmail(payload.email, configurableFormFields?.confirm_email, formatMessage); + if (fieldError) { + fieldErrors.email = fieldError; + isValid = false; + } + if (confirmEmailError) { + fieldErrors.confirm_email = confirmEmailError; + isValid = false; + } + emailSuggestion = suggestion; + break; } - if (fieldErrors[key]) { - isValid = false; + case 'username': + fieldErrors.username = validateUsername(payload.username, formatMessage); + if (fieldErrors.username) { isValid = false; } + break; + case 'password': + fieldErrors.password = validatePasswordField(payload.password, formatMessage); + if (fieldErrors.password) { isValid = false; } + break; + default: + break; } }); @@ -59,12 +89,10 @@ export const isFormValid = ( } else if (!configurableFormFields[key]) { fieldErrors[key] = fieldDescriptions[key].error_message; } - if (fieldErrors[key]) { - isValid = false; - } + if (fieldErrors[key]) { isValid = false; } }); - return { isValid, fieldErrors }; + return { isValid, fieldErrors, emailSuggestion }; }; /**