Skip to content

Commit

Permalink
feat: final changes
Browse files Browse the repository at this point in the history
  • Loading branch information
syedsajjadkazmii committed Sep 7, 2023
1 parent 68be7c6 commit 7811f49
Show file tree
Hide file tree
Showing 21 changed files with 2,229 additions and 281 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import { mount } from 'enzyme';
import { BrowserRouter as Router } from 'react-router-dom';
import configureStore from 'redux-mock-store';

import ConfigurableRegistrationForm from '../ConfigurableRegistrationForm';
import { FIELDS } from '../../data/constants';
import ConfigurableRegistrationForm from '../ConfigurableRegistrationForm';

jest.mock('@edx/frontend-platform/analytics', () => ({
sendPageEvent: jest.fn(),
Expand Down
525 changes: 245 additions & 280 deletions src/register/components/tests/EmbeddableRegistrationPage.test.jsx

Large diffs are not rendered by default.

143 changes: 143 additions & 0 deletions src/register/registrationFields/CountryField/CountryField.jsx
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 src/register/registrationFields/CountryField/CountryField.test.jsx
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');
});
});
});
29 changes: 29 additions & 0 deletions src/register/registrationFields/CountryField/validator.js
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;
Loading

0 comments on commit 7811f49

Please sign in to comment.