Skip to content

Commit 7811f49

Browse files
feat: final changes
1 parent 68be7c6 commit 7811f49

21 files changed

+2229
-281
lines changed

src/register/components/tests/ConfigurableRegistrationForm.test.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ import { mount } from 'enzyme';
99
import { BrowserRouter as Router } from 'react-router-dom';
1010
import configureStore from 'redux-mock-store';
1111

12-
import ConfigurableRegistrationForm from '../ConfigurableRegistrationForm';
1312
import { FIELDS } from '../../data/constants';
13+
import ConfigurableRegistrationForm from '../ConfigurableRegistrationForm';
1414

1515
jest.mock('@edx/frontend-platform/analytics', () => ({
1616
sendPageEvent: jest.fn(),

src/register/components/tests/EmbeddableRegistrationPage.test.jsx

Lines changed: 245 additions & 280 deletions
Large diffs are not rendered by default.
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import React, { useEffect } from 'react';
2+
import { useDispatch, useSelector } from 'react-redux';
3+
4+
import { useIntl } from '@edx/frontend-platform/i18n';
5+
import { FormAutosuggest, FormAutosuggestOption, FormControlFeedback } from '@edx/paragon';
6+
import PropTypes from 'prop-types';
7+
8+
import validateCountryField, { COUNTRY_CODE_KEY, COUNTRY_DISPLAY_KEY } from './validator';
9+
import { clearRegistrationBackendError } from '../../data/actions';
10+
import messages from '../../messages';
11+
12+
/**
13+
* Country field wrapper. It accepts following handlers
14+
* - handleChange for setting value change and
15+
* - handleErrorChange for setting error
16+
*
17+
* It is responsible for
18+
* - Auto populating country field if backendCountryCode is available in redux
19+
* - Performing country field validations
20+
* - clearing error on focus
21+
* - setting value on change and selection
22+
*/
23+
const CountryField = (props) => {
24+
const {
25+
countryList,
26+
selectedCountry,
27+
onChangeHandler,
28+
handleErrorChange,
29+
onFocusHandler,
30+
} = props;
31+
const { formatMessage } = useIntl();
32+
const dispatch = useDispatch();
33+
const backendCountryCode = useSelector(state => state.register.backendCountryCode);
34+
35+
useEffect(() => {
36+
if (backendCountryCode && backendCountryCode !== selectedCountry?.countryCode) {
37+
let countryCode = '';
38+
let countryDisplayValue = '';
39+
40+
const countryVal = countryList.find(
41+
(country) => (country[COUNTRY_CODE_KEY].toLowerCase() === backendCountryCode.toLowerCase()),
42+
);
43+
if (countryVal) {
44+
countryCode = countryVal[COUNTRY_CODE_KEY];
45+
countryDisplayValue = countryVal[COUNTRY_DISPLAY_KEY];
46+
}
47+
onChangeHandler(
48+
{ target: { name: 'country' } },
49+
{ countryCode, displayValue: countryDisplayValue },
50+
);
51+
}
52+
}, [backendCountryCode, countryList]); // eslint-disable-line react-hooks/exhaustive-deps
53+
54+
const handleOnBlur = (event) => {
55+
// Do not run validations when drop-down arrow is clicked
56+
if (event.relatedTarget && event.relatedTarget.className.includes('pgn__form-autosuggest__icon-button')) {
57+
return;
58+
}
59+
60+
const { value } = event.target;
61+
62+
const { countryCode, displayValue, error } = validateCountryField(
63+
value.trim(), countryList, formatMessage(messages['empty.country.field.error']),
64+
);
65+
66+
onChangeHandler({ target: { name: 'country' } }, { countryCode, displayValue });
67+
handleErrorChange('country', error);
68+
};
69+
70+
const handleSelected = (value) => {
71+
handleOnBlur({ target: { name: 'country', value } });
72+
};
73+
74+
const handleOnFocus = (event) => {
75+
handleErrorChange('country', '');
76+
dispatch(clearRegistrationBackendError('country'));
77+
onFocusHandler(event);
78+
};
79+
80+
const handleOnChange = (value) => {
81+
onChangeHandler({ target: { name: 'country' } }, { countryCode: '', displayValue: value });
82+
};
83+
84+
const getCountryList = () => countryList.map((country) => (
85+
<FormAutosuggestOption key={country[COUNTRY_CODE_KEY]}>
86+
{country[COUNTRY_DISPLAY_KEY]}
87+
</FormAutosuggestOption>
88+
));
89+
90+
return (
91+
<div className="mb-4">
92+
<FormAutosuggest
93+
floatingLabel={formatMessage(messages['registration.country.label'])}
94+
aria-label="form autosuggest"
95+
name="country"
96+
value={selectedCountry.displayValue || ''}
97+
onSelected={(value) => handleSelected(value)}
98+
onFocus={(e) => handleOnFocus(e)}
99+
onBlur={(e) => handleOnBlur(e)}
100+
onChange={(value) => handleOnChange(value)}
101+
>
102+
{getCountryList()}
103+
</FormAutosuggest>
104+
{props.errorMessage !== '' && (
105+
<FormControlFeedback
106+
key="error"
107+
className="form-text-size"
108+
hasIcon={false}
109+
feedback-for="country"
110+
type="invalid"
111+
>
112+
{props.errorMessage}
113+
</FormControlFeedback>
114+
)}
115+
</div>
116+
);
117+
};
118+
119+
CountryField.propTypes = {
120+
countryList: PropTypes.arrayOf(
121+
PropTypes.shape({
122+
code: PropTypes.string,
123+
name: PropTypes.string,
124+
}),
125+
).isRequired,
126+
errorMessage: PropTypes.string,
127+
onChangeHandler: PropTypes.func.isRequired,
128+
handleErrorChange: PropTypes.func.isRequired,
129+
onFocusHandler: PropTypes.func.isRequired,
130+
selectedCountry: PropTypes.shape({
131+
displayValue: PropTypes.string,
132+
countryCode: PropTypes.string,
133+
}),
134+
};
135+
136+
CountryField.defaultProps = {
137+
errorMessage: null,
138+
selectedCountry: {
139+
value: '',
140+
},
141+
};
142+
143+
export default CountryField;
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
import React from 'react';
2+
import { Provider } from 'react-redux';
3+
4+
import { mergeConfig } from '@edx/frontend-platform';
5+
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
6+
import { mount } from 'enzyme';
7+
import { BrowserRouter as Router } from 'react-router-dom';
8+
import configureStore from 'redux-mock-store';
9+
10+
import { COUNTRY_CODE_KEY, COUNTRY_DISPLAY_KEY } from './validator';
11+
import { CountryField } from '../index';
12+
13+
const IntlCountryField = injectIntl(CountryField);
14+
const mockStore = configureStore();
15+
16+
jest.mock('react-router-dom', () => {
17+
const mockNavigation = jest.fn();
18+
19+
// eslint-disable-next-line react/prop-types
20+
const Navigate = ({ to }) => {
21+
mockNavigation(to);
22+
return <div />;
23+
};
24+
25+
return {
26+
...jest.requireActual('react-router-dom'),
27+
Navigate,
28+
mockNavigate: mockNavigation,
29+
};
30+
});
31+
32+
describe('CountryField', () => {
33+
let props = {};
34+
let store = {};
35+
36+
const reduxWrapper = children => (
37+
<IntlProvider locale="en">
38+
<Provider store={store}>{children}</Provider>
39+
</IntlProvider>
40+
);
41+
42+
const routerWrapper = children => (
43+
<Router>
44+
{children}
45+
</Router>
46+
);
47+
48+
const initialState = {
49+
register: {},
50+
};
51+
52+
beforeEach(() => {
53+
store = mockStore(initialState);
54+
props = {
55+
countryList: [{
56+
[COUNTRY_CODE_KEY]: 'PK',
57+
[COUNTRY_DISPLAY_KEY]: 'Pakistan',
58+
}],
59+
selectedCountry: {
60+
countryCode: '',
61+
displayValue: '',
62+
},
63+
errorMessage: '',
64+
onChangeHandler: jest.fn(),
65+
handleErrorChange: jest.fn(),
66+
onFocusHandler: jest.fn(),
67+
};
68+
window.location = { search: '' };
69+
});
70+
71+
afterEach(() => {
72+
jest.clearAllMocks();
73+
});
74+
75+
describe('Test Country Field', () => {
76+
mergeConfig({
77+
SHOW_CONFIGURABLE_EDX_FIELDS: true,
78+
});
79+
80+
const emptyFieldValidation = {
81+
country: 'Select your country or region of residence',
82+
};
83+
84+
it('should run country field validation when onBlur is fired', () => {
85+
const countryField = mount(routerWrapper(reduxWrapper(<IntlCountryField {...props} />)));
86+
countryField.find('input[name="country"]').simulate('blur', { target: { value: '', name: 'country' } });
87+
expect(props.handleErrorChange).toHaveBeenCalledTimes(1);
88+
expect(props.handleErrorChange).toHaveBeenCalledWith(
89+
'country',
90+
emptyFieldValidation.country,
91+
);
92+
});
93+
94+
it('should not run country field validation when onBlur is fired by drop-down arrow icon click', () => {
95+
const countryField = mount(routerWrapper(reduxWrapper(<IntlCountryField {...props} />)));
96+
countryField.find('input[name="country"]').simulate('blur', {
97+
target: { value: '', name: 'country' },
98+
relatedTarget: { type: 'button', className: 'btn-icon pgn__form-autosuggest__icon-button' },
99+
});
100+
expect(props.handleErrorChange).toHaveBeenCalledTimes(0);
101+
});
102+
103+
it('should update errors for frontend validations', () => {
104+
const countryField = mount(routerWrapper(reduxWrapper(<IntlCountryField {...props} />)));
105+
106+
countryField.find('input[name="country"]').simulate('blur', { target: { value: '', name: 'country' } });
107+
expect(props.handleErrorChange).toHaveBeenCalledTimes(1);
108+
expect(props.handleErrorChange).toHaveBeenCalledWith(
109+
'country',
110+
emptyFieldValidation.country,
111+
);
112+
});
113+
114+
it('should clear error on focus', () => {
115+
const countryField = mount(routerWrapper(reduxWrapper(<IntlCountryField {...props} />)));
116+
117+
countryField.find('input[name="country"]').simulate('focus', { target: { value: '', name: 'country' } });
118+
expect(props.handleErrorChange).toHaveBeenCalledTimes(1);
119+
expect(props.handleErrorChange).toHaveBeenCalledWith(
120+
'country',
121+
'',
122+
);
123+
});
124+
125+
it('should update state from country code present in redux store', () => {
126+
store = mockStore({
127+
...initialState,
128+
register: {
129+
...initialState.register,
130+
backendCountryCode: 'PK',
131+
},
132+
});
133+
134+
mount(routerWrapper(reduxWrapper(<IntlCountryField {...props} />)));
135+
expect(props.onChangeHandler).toHaveBeenCalledTimes(1);
136+
expect(props.onChangeHandler).toHaveBeenCalledWith(
137+
{ target: { name: 'country' } },
138+
{ countryCode: 'PK', displayValue: 'Pakistan' },
139+
);
140+
});
141+
142+
it('should set option on dropdown menu item click', () => {
143+
const countryField = mount(routerWrapper(reduxWrapper(<IntlCountryField {...props} />)));
144+
145+
countryField.find('.pgn__form-autosuggest__icon-button').first().simulate('click');
146+
countryField.find('.dropdown-item').first().simulate('click');
147+
148+
expect(props.onChangeHandler).toHaveBeenCalledTimes(1);
149+
expect(props.onChangeHandler).toHaveBeenCalledWith(
150+
{ target: { name: 'country' } },
151+
{ countryCode: 'PK', displayValue: 'Pakistan' },
152+
);
153+
});
154+
155+
it('should set value on change', () => {
156+
const countryField = mount(routerWrapper(reduxWrapper(<IntlCountryField {...props} />)));
157+
158+
countryField.find('input[name="country"]').simulate(
159+
'change', { target: { value: 'pak', name: 'country' } },
160+
);
161+
162+
expect(props.onChangeHandler).toHaveBeenCalledTimes(1);
163+
expect(props.onChangeHandler).toHaveBeenCalledWith(
164+
{ target: { name: 'country' } },
165+
{ countryCode: '', displayValue: 'pak' },
166+
);
167+
});
168+
169+
it('should display error on invalid country input', () => {
170+
props = {
171+
...props,
172+
errorMessage: 'country error message',
173+
};
174+
175+
const countryField = mount(routerWrapper(reduxWrapper(<IntlCountryField {...props} />)));
176+
177+
expect(countryField.find('div[feedback-for="country"]').text()).toEqual('country error message');
178+
});
179+
});
180+
});
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
export const COUNTRY_CODE_KEY = 'code';
2+
export const COUNTRY_DISPLAY_KEY = 'name';
3+
4+
const validateCountryField = (value, countryList, errorMessage) => {
5+
let countryCode = '';
6+
let displayValue = value;
7+
let error = errorMessage;
8+
9+
if (value) {
10+
const normalizedValue = value.toLowerCase();
11+
// Handling a case here where user enters a valid country code that needs to be
12+
// evaluated and set its value as a valid value.
13+
const selectedCountry = countryList.find(
14+
(country) => (
15+
// When translations are applied, extra space added in country value, so we should trim that.
16+
country[COUNTRY_DISPLAY_KEY].toLowerCase().trim() === normalizedValue
17+
|| country[COUNTRY_CODE_KEY].toLowerCase().trim() === normalizedValue
18+
),
19+
);
20+
if (selectedCountry) {
21+
countryCode = selectedCountry[COUNTRY_CODE_KEY];
22+
displayValue = selectedCountry[COUNTRY_DISPLAY_KEY];
23+
error = '';
24+
}
25+
}
26+
return { error, countryCode, displayValue };
27+
};
28+
29+
export default validateCountryField;

0 commit comments

Comments
 (0)