diff --git a/src/register/RegistrationFields/NameField/NameField.jsx b/src/register/RegistrationFields/NameField/NameField.jsx index ec2eb8552e..9a03b6cf1e 100644 --- a/src/register/RegistrationFields/NameField/NameField.jsx +++ b/src/register/RegistrationFields/NameField/NameField.jsx @@ -27,21 +27,23 @@ const NameField = (props) => { const { handleErrorChange, shouldFetchUsernameSuggestions, + name, + fullName, } = props; const handleOnBlur = (e) => { const { value } = e.target; - const fieldError = validateName(value, formatMessage); + const fieldError = validateName(value, name, formatMessage); if (fieldError) { - handleErrorChange('name', fieldError); + handleErrorChange(name, fieldError); } else if (shouldFetchUsernameSuggestions && !validationApiRateLimited) { - dispatch(fetchRealtimeValidations({ name: value })); + dispatch(fetchRealtimeValidations({ name: fullName.trim() })); } }; const handleOnFocus = () => { - handleErrorChange('name', ''); - dispatch(clearRegistrationBackendError('name')); + handleErrorChange(name, ''); + dispatch(clearRegistrationBackendError(name)); }; return ( @@ -56,6 +58,7 @@ const NameField = (props) => { NameField.defaultProps = { errorMessage: '', shouldFetchUsernameSuggestions: false, + fullName: '', }; NameField.propTypes = { @@ -64,6 +67,8 @@ NameField.propTypes = { value: PropTypes.string.isRequired, handleChange: PropTypes.func.isRequired, handleErrorChange: PropTypes.func.isRequired, + name: PropTypes.string.isRequired, + fullName: PropTypes.string, }; export default NameField; diff --git a/src/register/RegistrationFields/NameField/NameField.test.jsx b/src/register/RegistrationFields/NameField/NameField.test.jsx index 3d33467229..eb52754413 100644 --- a/src/register/RegistrationFields/NameField/NameField.test.jsx +++ b/src/register/RegistrationFields/NameField/NameField.test.jsx @@ -51,7 +51,7 @@ describe('NameField', () => { beforeEach(() => { store = mockStore(initialState); props = { - name: 'name', + name: '', value: '', errorMessage: '', handleChange: jest.fn(), @@ -66,43 +66,44 @@ describe('NameField', () => { }); describe('Test Name Field', () => { - const fieldValidation = { name: 'Enter your full name' }; - - it('should run name field validation when onBlur is fired', () => { + it('should run first name field validation when onBlur is fired', () => { + props.name = 'firstName'; const { container } = render(routerWrapper(reduxWrapper())); - const nameInput = container.querySelector('input#name'); - fireEvent.blur(nameInput, { target: { value: '', name: 'name' } }); + const firstNameInput = container.querySelector('input#firstName'); + fireEvent.blur(firstNameInput, { target: { value: '', name: 'firstName' } }); expect(props.handleErrorChange).toHaveBeenCalledTimes(1); expect(props.handleErrorChange).toHaveBeenCalledWith( - 'name', - fieldValidation.name, + 'firstName', + 'Enter your first name', ); }); - it('should update errors for frontend validations', () => { + it('should update first name field error for frontend validations', () => { + props.name = 'firstName'; const { container } = render(routerWrapper(reduxWrapper())); - const nameInput = container.querySelector('input#name'); - fireEvent.blur(nameInput, { target: { value: 'https://invalid-name.com', name: 'name' } }); + const firstNameInput = container.querySelector('input#firstName'); + fireEvent.blur(firstNameInput, { target: { value: 'https://invalid-name.com', name: 'firstName' } }); expect(props.handleErrorChange).toHaveBeenCalledTimes(1); expect(props.handleErrorChange).toHaveBeenCalledWith( - 'name', - 'Enter a valid name', + 'firstName', + 'Enter a valid first name', ); }); - it('should clear error on focus', () => { + it('should clear first name error on focus', () => { + props.name = 'firstName'; const { container } = render(routerWrapper(reduxWrapper())); - const nameInput = container.querySelector('input#name'); - fireEvent.focus(nameInput, { target: { value: '', name: 'name' } }); + const firstNameInput = container.querySelector('input#firstName'); + fireEvent.focus(firstNameInput, { target: { value: '', name: 'firstName' } }); expect(props.handleErrorChange).toHaveBeenCalledTimes(1); expect(props.handleErrorChange).toHaveBeenCalledWith( - 'name', + 'firstName', '', ); }); @@ -112,14 +113,16 @@ describe('NameField', () => { props = { ...props, shouldFetchUsernameSuggestions: true, + fullName: 'test test', }; + props.name = 'lastName'; const { container } = render(routerWrapper(reduxWrapper())); - const nameInput = container.querySelector('input#name'); + const lastNameInput = container.querySelector('input#lastName'); // Enter a valid name so that frontend validations are passed - fireEvent.blur(nameInput, { target: { value: 'test', name: 'name' } }); + fireEvent.blur(lastNameInput, { target: { value: 'test', name: 'lastName' } }); - expect(store.dispatch).toHaveBeenCalledWith(fetchRealtimeValidations({ name: 'test' })); + expect(store.dispatch).toHaveBeenCalledWith(fetchRealtimeValidations({ name: props.fullName })); }); it('should clear the registration validation error on focus on field', () => { @@ -134,14 +137,43 @@ describe('NameField', () => { }, }); + props.name = 'lastName'; store.dispatch = jest.fn(store.dispatch); const { container } = render(routerWrapper(reduxWrapper())); - const nameInput = container.querySelector('input#name'); + const lastNameInput = container.querySelector('input#lastName'); + + fireEvent.focus(lastNameInput, { target: { value: 'test', name: 'lastName' } }); - fireEvent.focus(nameInput, { target: { value: 'test', name: 'name' } }); + expect(store.dispatch).toHaveBeenCalledWith(clearRegistrationBackendError('lastName')); + }); + + it('should run last name field validation when onBlur is fired', () => { + props.name = 'lastName'; + const { container } = render(routerWrapper(reduxWrapper())); - expect(store.dispatch).toHaveBeenCalledWith(clearRegistrationBackendError('name')); + const lastNameInput = container.querySelector('input#lastName'); + fireEvent.blur(lastNameInput, { target: { value: '', name: 'lastName' } }); + + expect(props.handleErrorChange).toHaveBeenCalledTimes(1); + expect(props.handleErrorChange).toHaveBeenCalledWith( + 'lastName', + 'Enter your last name', + ); + }); + + it('should update last name field error for frontend validation', () => { + props.name = 'lastName'; + const { container } = render(routerWrapper(reduxWrapper())); + + const lastNameInput = container.querySelector('input#lastName'); + fireEvent.blur(lastNameInput, { target: { value: 'https://invalid-name.com', name: 'lastName' } }); + + expect(props.handleErrorChange).toHaveBeenCalledTimes(1); + expect(props.handleErrorChange).toHaveBeenCalledWith( + 'lastName', + 'Enter a valid last name', + ); }); }); }); diff --git a/src/register/RegistrationFields/NameField/validator.js b/src/register/RegistrationFields/NameField/validator.js index e62c227d08..36036bf924 100644 --- a/src/register/RegistrationFields/NameField/validator.js +++ b/src/register/RegistrationFields/NameField/validator.js @@ -9,12 +9,16 @@ export const HTML_REGEX = /<|>/u; // regex from backend export const INVALID_NAME_REGEX = /https?:\/\/(?:[-\w.]|(?:%[\da-fA-F]{2}))*/g; -const validateName = (value, formatMessage) => { +const validateName = (value, fieldName, formatMessage) => { let fieldError; if (!value.trim()) { - fieldError = formatMessage(messages['empty.name.field.error']); + fieldError = fieldName === 'lastName' + ? formatMessage(messages['empty.lastName.field.error']) + : formatMessage(messages['empty.firstName.field.error']); } else if (URL_REGEX.test(value) || HTML_REGEX.test(value) || INVALID_NAME_REGEX.test(value)) { - fieldError = formatMessage(messages['name.validation.message']); + fieldError = fieldName === 'lastName' + ? formatMessage(messages['lastName.validation.message']) + : formatMessage(messages['firstName.validation.message']); } return fieldError; }; diff --git a/src/register/RegistrationPage.jsx b/src/register/RegistrationPage.jsx index 43fc3df8b5..c5e616cdbf 100644 --- a/src/register/RegistrationPage.jsx +++ b/src/register/RegistrationPage.jsx @@ -119,9 +119,11 @@ const RegistrationPage = (props) => { setErrorCode(prevState => ({ type: TPA_AUTHENTICATION_FAILURE, count: prevState.count + 1 })); } if (pipelineUserDetails && Object.keys(pipelineUserDetails).length !== 0) { - const { name = '', username = '', email = '' } = pipelineUserDetails; + const { + firstName = '', lastName = '', username = '', email = '', + } = pipelineUserDetails; setFormFields(prevState => ({ - ...prevState, name, username, email, + ...prevState, firstName, lastName, username, email, })); dispatch(setUserPipelineDataLoaded(true)); } @@ -310,14 +312,22 @@ const RegistrationPage = (props) => { />
+ { marketingEmailsOptIn: true, }, formFields: { - name: '', email: '', username: '', password: '', + firstName: '', lastName: '', email: '', username: '', password: '', }, emailSuggestion: { suggestion: '', type: '', }, errors: { - name: '', email: '', username: '', password: '', + firstName: '', lastName: '', email: '', username: '', password: '', }, }; @@ -134,7 +134,8 @@ describe('RegistrationPage', () => { }); const populateRequiredFields = (getByLabelText, payload, isThirdPartyAuth = false) => { - fireEvent.change(getByLabelText('Full name'), { target: { value: payload.name, name: 'name' } }); + fireEvent.change(getByLabelText('First name'), { target: { value: payload.first_name, name: 'firstName' } }); + fireEvent.change(getByLabelText('Last name'), { target: { value: payload.last_name, name: 'lastName' } }); fireEvent.change(getByLabelText('Public username'), { target: { value: payload.username, name: 'username' } }); fireEvent.change(getByLabelText('Email'), { target: { value: payload.email, name: 'email' } }); @@ -152,7 +153,8 @@ describe('RegistrationPage', () => { }); const emptyFieldValidation = { - name: 'Enter your full name', + firstName: 'Enter your first name', + lastName: 'Enter your last name', username: 'Username must be between 2 and 30 characters', email: 'Enter your email', password: 'Password criteria has not been met', @@ -169,7 +171,8 @@ describe('RegistrationPage', () => { window.location = { href: getConfig().BASE_URL, search: '?next=/course/demo-course-url' }; const payload = { - name: 'John Doe', + first_name: 'John', + last_name: 'Doe', username: 'john_doe', email: 'john.doe@gmail.com', password: 'password1', @@ -192,7 +195,8 @@ describe('RegistrationPage', () => { jest.spyOn(global.Date, 'now').mockImplementation(() => 0); const formPayload = { - name: 'John Doe', + first_name: 'John', + last_name: 'Doe', username: 'john_doe', email: 'john.doe@example.com', country: 'Pakistan', @@ -228,7 +232,8 @@ describe('RegistrationPage', () => { jest.spyOn(global.Date, 'now').mockImplementation(() => 0); const payload = { - name: 'John Doe', + first_name: 'John', + last_name: 'Doe', username: 'john_doe', email: 'john.doe@gmail.com', password: 'password1', @@ -579,7 +584,8 @@ describe('RegistrationPage', () => { registrationFormData: { ...registrationFormData, formFields: { - name: 'John Doe', + firstName: 'John', + lastName: 'Doe', username: 'john_doe', email: 'john.doe@yopmail.com', password: 'password1', @@ -593,13 +599,15 @@ describe('RegistrationPage', () => { const { container } = render(routerWrapper(reduxWrapper())); - const fullNameInput = container.querySelector('input#name'); + const firstNameInput = container.querySelector('input#firstName'); + const lastNameInput = container.querySelector('input#lastName'); const usernameInput = container.querySelector('input#username'); const emailInput = container.querySelector('input#email'); const passwordInput = container.querySelector('input#password'); const emailSuggestion = container.querySelector('.email-suggestion-alert-warning'); - expect(fullNameInput.value).toEqual('John Doe'); + expect(firstNameInput.value).toEqual('John'); + expect(lastNameInput.value).toEqual('Doe'); expect(usernameInput.value).toEqual('john_doe'); expect(emailInput.value).toEqual('john.doe@yopmail.com'); expect(passwordInput.value).toEqual('password1'); @@ -720,7 +728,8 @@ describe('RegistrationPage', () => { thirdPartyAuthContext: { ...initialState.commonComponents.thirdPartyAuthContext, pipelineUserDetails: { - name: 'John Doe', + firstName: 'John', + lastName: 'Doe', username: 'john_doe', email: 'john.doe@example.com', }, @@ -751,7 +760,8 @@ describe('RegistrationPage', () => { registrationFormData: { ...registrationFormData, formFields: { - name: 'John Doe', + firstName: 'John', + lastName: 'Doe', username: 'john_doe', email: 'john.doe@example.com', }, @@ -771,7 +781,8 @@ describe('RegistrationPage', () => { ...initialState.commonComponents.thirdPartyAuthContext, currentProvider: 'Apple', pipelineUserDetails: { - name: 'John Doe', + firstName: 'John', + lastName: 'Doe', username: 'john_doe', email: 'john.doe@example.com', }, @@ -783,7 +794,8 @@ describe('RegistrationPage', () => { render(routerWrapper(reduxWrapper())); expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ - name: 'John Doe', + first_name: 'John', + last_name: 'Doe', username: 'john_doe', email: 'john.doe@example.com', country: 'PK', diff --git a/src/register/components/tests/ConfigurableRegistrationForm.test.jsx b/src/register/components/tests/ConfigurableRegistrationForm.test.jsx index 88d3e39f97..1f6949eef2 100644 --- a/src/register/components/tests/ConfigurableRegistrationForm.test.jsx +++ b/src/register/components/tests/ConfigurableRegistrationForm.test.jsx @@ -56,13 +56,13 @@ describe('ConfigurableRegistrationForm', () => { marketingEmailsOptIn: true, }, formFields: { - name: '', email: '', username: '', password: '', + firstName: '', lastName: '', email: '', username: '', password: '', }, emailSuggestion: { suggestion: '', type: '', }, errors: { - name: '', email: '', username: '', password: '', + firstName: '', lastName: '', email: '', username: '', password: '', }, }; @@ -128,7 +128,8 @@ describe('ConfigurableRegistrationForm', () => { }); const populateRequiredFields = (getByLabelText, payload, isThirdPartyAuth = false) => { - fireEvent.change(getByLabelText('Full name'), { target: { value: payload.name, name: 'name' } }); + fireEvent.change(getByLabelText('First name'), { target: { value: payload.first_name, name: 'firstName' } }); + fireEvent.change(getByLabelText('Last name'), { target: { value: payload.last_name, name: 'lastName' } }); fireEvent.change(getByLabelText('Public username'), { target: { value: payload.username, name: 'username' } }); fireEvent.change(getByLabelText('Email'), { target: { value: payload.email, name: 'email' } }); @@ -238,7 +239,8 @@ describe('ConfigurableRegistrationForm', () => { }); const payload = { - name: 'John Doe', + first_name: 'John', + last_name: 'Doe', username: 'john_doe', email: 'john.doe@example.com', password: 'password1', diff --git a/src/register/data/reducers.js b/src/register/data/reducers.js index e2cc00956a..17e063f853 100644 --- a/src/register/data/reducers.js +++ b/src/register/data/reducers.js @@ -22,13 +22,13 @@ export const defaultState = { marketingEmailsOptIn: true, }, formFields: { - name: '', email: '', username: '', password: '', + firstName: '', lastName: '', email: '', username: '', password: '', }, emailSuggestion: { suggestion: '', type: '', }, errors: { - name: '', email: '', username: '', password: '', + firstName: '', lastName: '', email: '', username: '', password: '', }, }, validations: null, diff --git a/src/register/data/tests/reducers.test.js b/src/register/data/tests/reducers.test.js index 07badb9cda..0844b00585 100644 --- a/src/register/data/tests/reducers.test.js +++ b/src/register/data/tests/reducers.test.js @@ -22,13 +22,13 @@ describe('Registration Reducer Tests', () => { marketingEmailsOptIn: true, }, formFields: { - name: '', email: '', username: '', password: '', + firstName: '', lastName: '', email: '', username: '', password: '', }, emailSuggestion: { suggestion: '', type: '', }, errors: { - name: '', email: '', username: '', password: '', + firstName: '', lastName: '', email: '', username: '', password: '', }, }, validations: null, diff --git a/src/register/messages.jsx b/src/register/messages.jsx index 39d9e7f549..600795d089 100644 --- a/src/register/messages.jsx +++ b/src/register/messages.jsx @@ -7,10 +7,15 @@ const messages = defineMessages({ description: 'register page title', }, // Field labels - 'registration.fullname.label': { - id: 'registration.fullname.label', - defaultMessage: 'Full name', - description: 'Label that appears above fullname field', + 'registration.firstName.label': { + id: 'registration.firstName.label', + defaultMessage: 'First name', + description: 'Label that appears above first name field', + }, + 'registration.lastName.label': { + id: 'registration.lastName.label', + defaultMessage: 'Last name', + description: 'Label that appears above last name field', }, 'registration.email.label': { id: 'registration.email.label', @@ -38,10 +43,10 @@ const messages = defineMessages({ description: 'Text for opt in option on register page.', }, // Help text - 'help.text.name': { - id: 'help.text.name', + 'help.text.firstName': { + id: 'help.text.firstName', defaultMessage: 'This name will be used by any certificates that you earn.', - description: 'Help text for fullname field on registration page', + description: 'Help text for first name field on registration page', }, 'help.text.username.1': { id: 'help.text.username.1', @@ -76,10 +81,15 @@ const messages = defineMessages({ description: 'Heading of institution page', }, // Validation messages - 'empty.name.field.error': { - id: 'empty.name.field.error', - defaultMessage: 'Enter your full name', - description: 'Error message for empty fullname field', + 'empty.firstName.field.error': { + id: 'empty.firstName.field.error', + defaultMessage: 'Enter your first name', + description: 'Error message for empty first name field', + }, + 'empty.lastName.field.error': { + id: 'empty.lastName.field.error', + defaultMessage: 'Enter your last name', + description: 'Error message for empty last name field', }, 'empty.email.field.error': { id: 'empty.email.field.error', @@ -121,10 +131,15 @@ const messages = defineMessages({ defaultMessage: 'Username must be between 2 and 30 characters', description: 'Error message for empty username field', }, - 'name.validation.message': { - id: 'name.validation.message', - defaultMessage: 'Enter a valid name', - description: 'Validation message that appears when fullname contain URL', + 'firstName.validation.message': { + id: 'firstName.validation.message', + defaultMessage: 'Enter a valid first name', + description: 'Validation message that appears when first name contain URL', + }, + 'lastName.validation.message': { + id: 'lastName.validation.message', + defaultMessage: 'Enter a valid last name', + description: 'Validation message that appears when last name contain URL', }, 'password.validation.message': { id: 'password.validation.message',