generated from openedx/frontend-template-application
-
Notifications
You must be signed in to change notification settings - Fork 165
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
68be7c6
commit 7811f49
Showing
21 changed files
with
2,229 additions
and
281 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
525 changes: 245 additions & 280 deletions
525
src/register/components/tests/EmbeddableRegistrationPage.test.jsx
Large diffs are not rendered by default.
Oops, something went wrong.
143 changes: 143 additions & 0 deletions
143
src/register/registrationFields/CountryField/CountryField.jsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
import React, { useEffect } from 'react'; | ||
import { useDispatch, useSelector } from 'react-redux'; | ||
|
||
import { useIntl } from '@edx/frontend-platform/i18n'; | ||
import { FormAutosuggest, FormAutosuggestOption, FormControlFeedback } from '@edx/paragon'; | ||
import PropTypes from 'prop-types'; | ||
|
||
import validateCountryField, { COUNTRY_CODE_KEY, COUNTRY_DISPLAY_KEY } from './validator'; | ||
import { clearRegistrationBackendError } from '../../data/actions'; | ||
import messages from '../../messages'; | ||
|
||
/** | ||
* Country field wrapper. It accepts following handlers | ||
* - handleChange for setting value change and | ||
* - handleErrorChange for setting error | ||
* | ||
* It is responsible for | ||
* - Auto populating country field if backendCountryCode is available in redux | ||
* - Performing country field validations | ||
* - clearing error on focus | ||
* - setting value on change and selection | ||
*/ | ||
const CountryField = (props) => { | ||
const { | ||
countryList, | ||
selectedCountry, | ||
onChangeHandler, | ||
handleErrorChange, | ||
onFocusHandler, | ||
} = props; | ||
const { formatMessage } = useIntl(); | ||
const dispatch = useDispatch(); | ||
const backendCountryCode = useSelector(state => state.register.backendCountryCode); | ||
|
||
useEffect(() => { | ||
if (backendCountryCode && backendCountryCode !== selectedCountry?.countryCode) { | ||
let countryCode = ''; | ||
let countryDisplayValue = ''; | ||
|
||
const countryVal = countryList.find( | ||
(country) => (country[COUNTRY_CODE_KEY].toLowerCase() === backendCountryCode.toLowerCase()), | ||
); | ||
if (countryVal) { | ||
countryCode = countryVal[COUNTRY_CODE_KEY]; | ||
countryDisplayValue = countryVal[COUNTRY_DISPLAY_KEY]; | ||
} | ||
onChangeHandler( | ||
{ target: { name: 'country' } }, | ||
{ countryCode, displayValue: countryDisplayValue }, | ||
); | ||
} | ||
}, [backendCountryCode, countryList]); // eslint-disable-line react-hooks/exhaustive-deps | ||
|
||
const handleOnBlur = (event) => { | ||
// Do not run validations when drop-down arrow is clicked | ||
if (event.relatedTarget && event.relatedTarget.className.includes('pgn__form-autosuggest__icon-button')) { | ||
return; | ||
} | ||
|
||
const { value } = event.target; | ||
|
||
const { countryCode, displayValue, error } = validateCountryField( | ||
value.trim(), countryList, formatMessage(messages['empty.country.field.error']), | ||
); | ||
|
||
onChangeHandler({ target: { name: 'country' } }, { countryCode, displayValue }); | ||
handleErrorChange('country', error); | ||
}; | ||
|
||
const handleSelected = (value) => { | ||
handleOnBlur({ target: { name: 'country', value } }); | ||
}; | ||
|
||
const handleOnFocus = (event) => { | ||
handleErrorChange('country', ''); | ||
dispatch(clearRegistrationBackendError('country')); | ||
onFocusHandler(event); | ||
}; | ||
|
||
const handleOnChange = (value) => { | ||
onChangeHandler({ target: { name: 'country' } }, { countryCode: '', displayValue: value }); | ||
}; | ||
|
||
const getCountryList = () => countryList.map((country) => ( | ||
<FormAutosuggestOption key={country[COUNTRY_CODE_KEY]}> | ||
{country[COUNTRY_DISPLAY_KEY]} | ||
</FormAutosuggestOption> | ||
)); | ||
|
||
return ( | ||
<div className="mb-4"> | ||
<FormAutosuggest | ||
floatingLabel={formatMessage(messages['registration.country.label'])} | ||
aria-label="form autosuggest" | ||
name="country" | ||
value={selectedCountry.displayValue || ''} | ||
onSelected={(value) => handleSelected(value)} | ||
onFocus={(e) => handleOnFocus(e)} | ||
onBlur={(e) => handleOnBlur(e)} | ||
onChange={(value) => handleOnChange(value)} | ||
> | ||
{getCountryList()} | ||
</FormAutosuggest> | ||
{props.errorMessage !== '' && ( | ||
<FormControlFeedback | ||
key="error" | ||
className="form-text-size" | ||
hasIcon={false} | ||
feedback-for="country" | ||
type="invalid" | ||
> | ||
{props.errorMessage} | ||
</FormControlFeedback> | ||
)} | ||
</div> | ||
); | ||
}; | ||
|
||
CountryField.propTypes = { | ||
countryList: PropTypes.arrayOf( | ||
PropTypes.shape({ | ||
code: PropTypes.string, | ||
name: PropTypes.string, | ||
}), | ||
).isRequired, | ||
errorMessage: PropTypes.string, | ||
onChangeHandler: PropTypes.func.isRequired, | ||
handleErrorChange: PropTypes.func.isRequired, | ||
onFocusHandler: PropTypes.func.isRequired, | ||
selectedCountry: PropTypes.shape({ | ||
displayValue: PropTypes.string, | ||
countryCode: PropTypes.string, | ||
}), | ||
}; | ||
|
||
CountryField.defaultProps = { | ||
errorMessage: null, | ||
selectedCountry: { | ||
value: '', | ||
}, | ||
}; | ||
|
||
export default CountryField; |
180 changes: 180 additions & 0 deletions
180
src/register/registrationFields/CountryField/CountryField.test.jsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,180 @@ | ||
import React from 'react'; | ||
import { Provider } from 'react-redux'; | ||
|
||
import { mergeConfig } from '@edx/frontend-platform'; | ||
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n'; | ||
import { mount } from 'enzyme'; | ||
import { BrowserRouter as Router } from 'react-router-dom'; | ||
import configureStore from 'redux-mock-store'; | ||
|
||
import { COUNTRY_CODE_KEY, COUNTRY_DISPLAY_KEY } from './validator'; | ||
import { CountryField } from '../index'; | ||
|
||
const IntlCountryField = injectIntl(CountryField); | ||
const mockStore = configureStore(); | ||
|
||
jest.mock('react-router-dom', () => { | ||
const mockNavigation = jest.fn(); | ||
|
||
// eslint-disable-next-line react/prop-types | ||
const Navigate = ({ to }) => { | ||
mockNavigation(to); | ||
return <div />; | ||
}; | ||
|
||
return { | ||
...jest.requireActual('react-router-dom'), | ||
Navigate, | ||
mockNavigate: mockNavigation, | ||
}; | ||
}); | ||
|
||
describe('CountryField', () => { | ||
let props = {}; | ||
let store = {}; | ||
|
||
const reduxWrapper = children => ( | ||
<IntlProvider locale="en"> | ||
<Provider store={store}>{children}</Provider> | ||
</IntlProvider> | ||
); | ||
|
||
const routerWrapper = children => ( | ||
<Router> | ||
{children} | ||
</Router> | ||
); | ||
|
||
const initialState = { | ||
register: {}, | ||
}; | ||
|
||
beforeEach(() => { | ||
store = mockStore(initialState); | ||
props = { | ||
countryList: [{ | ||
[COUNTRY_CODE_KEY]: 'PK', | ||
[COUNTRY_DISPLAY_KEY]: 'Pakistan', | ||
}], | ||
selectedCountry: { | ||
countryCode: '', | ||
displayValue: '', | ||
}, | ||
errorMessage: '', | ||
onChangeHandler: jest.fn(), | ||
handleErrorChange: jest.fn(), | ||
onFocusHandler: jest.fn(), | ||
}; | ||
window.location = { search: '' }; | ||
}); | ||
|
||
afterEach(() => { | ||
jest.clearAllMocks(); | ||
}); | ||
|
||
describe('Test Country Field', () => { | ||
mergeConfig({ | ||
SHOW_CONFIGURABLE_EDX_FIELDS: true, | ||
}); | ||
|
||
const emptyFieldValidation = { | ||
country: 'Select your country or region of residence', | ||
}; | ||
|
||
it('should run country field validation when onBlur is fired', () => { | ||
const countryField = mount(routerWrapper(reduxWrapper(<IntlCountryField {...props} />))); | ||
countryField.find('input[name="country"]').simulate('blur', { target: { value: '', name: 'country' } }); | ||
expect(props.handleErrorChange).toHaveBeenCalledTimes(1); | ||
expect(props.handleErrorChange).toHaveBeenCalledWith( | ||
'country', | ||
emptyFieldValidation.country, | ||
); | ||
}); | ||
|
||
it('should not run country field validation when onBlur is fired by drop-down arrow icon click', () => { | ||
const countryField = mount(routerWrapper(reduxWrapper(<IntlCountryField {...props} />))); | ||
countryField.find('input[name="country"]').simulate('blur', { | ||
target: { value: '', name: 'country' }, | ||
relatedTarget: { type: 'button', className: 'btn-icon pgn__form-autosuggest__icon-button' }, | ||
}); | ||
expect(props.handleErrorChange).toHaveBeenCalledTimes(0); | ||
}); | ||
|
||
it('should update errors for frontend validations', () => { | ||
const countryField = mount(routerWrapper(reduxWrapper(<IntlCountryField {...props} />))); | ||
|
||
countryField.find('input[name="country"]').simulate('blur', { target: { value: '', name: 'country' } }); | ||
expect(props.handleErrorChange).toHaveBeenCalledTimes(1); | ||
expect(props.handleErrorChange).toHaveBeenCalledWith( | ||
'country', | ||
emptyFieldValidation.country, | ||
); | ||
}); | ||
|
||
it('should clear error on focus', () => { | ||
const countryField = mount(routerWrapper(reduxWrapper(<IntlCountryField {...props} />))); | ||
|
||
countryField.find('input[name="country"]').simulate('focus', { target: { value: '', name: 'country' } }); | ||
expect(props.handleErrorChange).toHaveBeenCalledTimes(1); | ||
expect(props.handleErrorChange).toHaveBeenCalledWith( | ||
'country', | ||
'', | ||
); | ||
}); | ||
|
||
it('should update state from country code present in redux store', () => { | ||
store = mockStore({ | ||
...initialState, | ||
register: { | ||
...initialState.register, | ||
backendCountryCode: 'PK', | ||
}, | ||
}); | ||
|
||
mount(routerWrapper(reduxWrapper(<IntlCountryField {...props} />))); | ||
expect(props.onChangeHandler).toHaveBeenCalledTimes(1); | ||
expect(props.onChangeHandler).toHaveBeenCalledWith( | ||
{ target: { name: 'country' } }, | ||
{ countryCode: 'PK', displayValue: 'Pakistan' }, | ||
); | ||
}); | ||
|
||
it('should set option on dropdown menu item click', () => { | ||
const countryField = mount(routerWrapper(reduxWrapper(<IntlCountryField {...props} />))); | ||
|
||
countryField.find('.pgn__form-autosuggest__icon-button').first().simulate('click'); | ||
countryField.find('.dropdown-item').first().simulate('click'); | ||
|
||
expect(props.onChangeHandler).toHaveBeenCalledTimes(1); | ||
expect(props.onChangeHandler).toHaveBeenCalledWith( | ||
{ target: { name: 'country' } }, | ||
{ countryCode: 'PK', displayValue: 'Pakistan' }, | ||
); | ||
}); | ||
|
||
it('should set value on change', () => { | ||
const countryField = mount(routerWrapper(reduxWrapper(<IntlCountryField {...props} />))); | ||
|
||
countryField.find('input[name="country"]').simulate( | ||
'change', { target: { value: 'pak', name: 'country' } }, | ||
); | ||
|
||
expect(props.onChangeHandler).toHaveBeenCalledTimes(1); | ||
expect(props.onChangeHandler).toHaveBeenCalledWith( | ||
{ target: { name: 'country' } }, | ||
{ countryCode: '', displayValue: 'pak' }, | ||
); | ||
}); | ||
|
||
it('should display error on invalid country input', () => { | ||
props = { | ||
...props, | ||
errorMessage: 'country error message', | ||
}; | ||
|
||
const countryField = mount(routerWrapper(reduxWrapper(<IntlCountryField {...props} />))); | ||
|
||
expect(countryField.find('div[feedback-for="country"]').text()).toEqual('country error message'); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
export const COUNTRY_CODE_KEY = 'code'; | ||
export const COUNTRY_DISPLAY_KEY = 'name'; | ||
|
||
const validateCountryField = (value, countryList, errorMessage) => { | ||
let countryCode = ''; | ||
let displayValue = value; | ||
let error = errorMessage; | ||
|
||
if (value) { | ||
const normalizedValue = value.toLowerCase(); | ||
// Handling a case here where user enters a valid country code that needs to be | ||
// evaluated and set its value as a valid value. | ||
const selectedCountry = countryList.find( | ||
(country) => ( | ||
// When translations are applied, extra space added in country value, so we should trim that. | ||
country[COUNTRY_DISPLAY_KEY].toLowerCase().trim() === normalizedValue | ||
|| country[COUNTRY_CODE_KEY].toLowerCase().trim() === normalizedValue | ||
), | ||
); | ||
if (selectedCountry) { | ||
countryCode = selectedCountry[COUNTRY_CODE_KEY]; | ||
displayValue = selectedCountry[COUNTRY_DISPLAY_KEY]; | ||
error = ''; | ||
} | ||
} | ||
return { error, countryCode, displayValue }; | ||
}; | ||
|
||
export default validateCountryField; |
Oops, something went wrong.