Skip to content

Commit d469102

Browse files
refactor: registration component refactoring (#1050)
* refactor: registration component refactoring * fix: refactored constants
1 parent c685bdd commit d469102

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+2697
-1657
lines changed

src/common-components/PasswordField.jsx

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import React, { useState } from 'react';
2+
import { useDispatch, useSelector } from 'react-redux';
23

34
import { useIntl } from '@edx/frontend-platform/i18n';
45
import {
@@ -11,31 +12,77 @@ import PropTypes from 'prop-types';
1112

1213
import messages from './messages';
1314
import { LETTER_REGEX, NUMBER_REGEX } from '../data/constants';
15+
import { clearRegistrationBackendError, fetchRealtimeValidations } from '../register/data/actions';
16+
import { validatePasswordField } from '../register/data/utils';
1417

1518
const PasswordField = (props) => {
1619
const { formatMessage } = useIntl();
20+
const dispatch = useDispatch();
21+
22+
const validationApiRateLimited = useSelector(state => state.register.validationApiRateLimited);
1723
const [isPasswordHidden, setHiddenTrue, setHiddenFalse] = useToggle(true);
1824
const [showTooltip, setShowTooltip] = useState(false);
1925

2026
const handleBlur = (e) => {
27+
if (e.target?.name === 'password' && e.relatedTarget?.name === 'passwordIcon') {
28+
return; // Do not validations on password icon click
29+
}
30+
2131
if (props.handleBlur) { props.handleBlur(e); }
2232
setShowTooltip(props.showRequirements && false);
33+
if (props.handleErrorChange) { // If rendering from register page
34+
const fieldError = validatePasswordField(e.target.value, formatMessage);
35+
if (fieldError) {
36+
props.handleErrorChange('password', fieldError);
37+
} else if (!validationApiRateLimited) {
38+
dispatch(fetchRealtimeValidations({ password: e.target.value }));
39+
}
40+
}
2341
};
2442

2543
const handleFocus = (e) => {
44+
if (e.target?.name === 'passwordIcon') {
45+
return; // Do not clear error on password icon focus
46+
}
47+
2648
if (props.handleFocus) {
2749
props.handleFocus(e);
2850
}
51+
if (props.handleErrorChange) {
52+
props.handleErrorChange('password', '');
53+
dispatch(clearRegistrationBackendError('password'));
54+
}
2955
setTimeout(() => setShowTooltip(props.showRequirements && true), 150);
3056
};
3157

3258
const HideButton = (
33-
<IconButton onFocus={handleFocus} onBlur={handleBlur} name="password" src={VisibilityOff} iconAs={Icon} onClick={setHiddenTrue} size="sm" variant="secondary" alt={formatMessage(messages['hide.password'])} />
59+
<IconButton
60+
onFocus={handleFocus}
61+
onBlur={handleBlur}
62+
name="passwordIcon"
63+
src={VisibilityOff}
64+
iconAs={Icon}
65+
onClick={setHiddenTrue}
66+
size="sm"
67+
variant="secondary"
68+
alt={formatMessage(messages['hide.password'])}
69+
/>
3470
);
3571

3672
const ShowButton = (
37-
<IconButton onFocus={handleFocus} onBlur={handleBlur} name="password" src={Visibility} iconAs={Icon} onClick={setHiddenFalse} size="sm" variant="secondary" alt={formatMessage(messages['show.password'])} />
73+
<IconButton
74+
onFocus={handleFocus}
75+
onBlur={handleBlur}
76+
name="passwordIcon"
77+
src={Visibility}
78+
iconAs={Icon}
79+
onClick={setHiddenFalse}
80+
size="sm"
81+
variant="secondary"
82+
alt={formatMessage(messages['show.password'])}
83+
/>
3884
);
85+
3986
const placement = window.innerWidth < 768 ? 'top' : 'left';
4087
const tooltip = (
4188
<Tooltip id={`password-requirement-${placement}`}>
@@ -89,6 +136,7 @@ PasswordField.defaultProps = {
89136
handleBlur: null,
90137
handleFocus: null,
91138
handleChange: () => {},
139+
handleErrorChange: null,
92140
showRequirements: true,
93141
autoComplete: null,
94142
};
@@ -100,6 +148,7 @@ PasswordField.propTypes = {
100148
handleBlur: PropTypes.func,
101149
handleFocus: PropTypes.func,
102150
handleChange: PropTypes.func,
151+
handleErrorChange: PropTypes.func,
103152
name: PropTypes.string.isRequired,
104153
showRequirements: PropTypes.bool,
105154
value: PropTypes.string.isRequired,

src/common-components/data/reducers.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export const defaultState = {
99
},
1010
thirdPartyAuthApiStatus: null,
1111
thirdPartyAuthContext: {
12+
autoSubmitRegForm: false,
1213
currentProvider: null,
1314
finishAuthUrl: null,
1415
countryCode: null,

src/common-components/tests/FormField.test.jsx

Lines changed: 137 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
import React from 'react';
2+
import { Provider } from 'react-redux';
23

34
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
45
import { mount } from 'enzyme';
56
import { act } from 'react-dom/test-utils';
7+
import { MemoryRouter } from 'react-router-dom';
8+
import configureStore from 'redux-mock-store';
69

10+
import { fetchRealtimeValidations } from '../../register/data/actions';
711
import FormGroup from '../FormGroup';
812
import PasswordField from '../PasswordField';
913

@@ -26,10 +30,27 @@ describe('FormGroup', () => {
2630
});
2731

2832
describe('PasswordField', () => {
33+
const mockStore = configureStore();
2934
const IntlPasswordField = injectIntl(PasswordField);
3035
let props = {};
36+
let store = {};
37+
38+
const reduxWrapper = children => (
39+
<IntlProvider locale="en">
40+
<MemoryRouter>
41+
<Provider store={store}>{children}</Provider>
42+
</MemoryRouter>
43+
</IntlProvider>
44+
);
45+
46+
const initialState = {
47+
register: {
48+
validationApiRateLimited: false,
49+
},
50+
};
3151

3252
beforeEach(() => {
53+
store = mockStore(initialState);
3354
props = {
3455
floatingLabel: 'Password',
3556
name: 'password',
@@ -39,7 +60,7 @@ describe('PasswordField', () => {
3960
});
4061

4162
it('should show/hide password on icon click', () => {
42-
const passwordField = mount(<IntlProvider locale="en"><IntlPasswordField {...props} /></IntlProvider>);
63+
const passwordField = mount(reduxWrapper(<IntlPasswordField {...props} />));
4364

4465
passwordField.find('button[aria-label="Show password"]').simulate('click');
4566
expect(passwordField.find('input').prop('type')).toEqual('text');
@@ -49,7 +70,7 @@ describe('PasswordField', () => {
4970
});
5071

5172
it('should show password requirement tooltip on focus', async () => {
52-
const passwordField = mount(<IntlProvider locale="en"><IntlPasswordField {...props} /></IntlProvider>);
73+
const passwordField = mount(reduxWrapper(<IntlPasswordField {...props} />));
5374
jest.useFakeTimers();
5475
await act(async () => {
5576
passwordField.find('input').simulate('focus');
@@ -67,7 +88,7 @@ describe('PasswordField', () => {
6788
};
6889

6990
jest.useFakeTimers();
70-
const passwordField = mount(<IntlProvider locale="en"><IntlPasswordField {...props} /></IntlProvider>);
91+
const passwordField = mount(reduxWrapper(<IntlPasswordField {...props} />));
7192
await act(async () => {
7293
passwordField.find('input').simulate('focus');
7394
jest.runAllTimers();
@@ -80,7 +101,7 @@ describe('PasswordField', () => {
80101
});
81102

82103
it('should update password requirement checks', async () => {
83-
const passwordField = mount(<IntlProvider locale="en"><IntlPasswordField {...props} /></IntlProvider>);
104+
const passwordField = mount(reduxWrapper(<IntlPasswordField {...props} />));
84105
jest.useFakeTimers();
85106
await act(async () => {
86107
passwordField.find('input').simulate('focus');
@@ -92,4 +113,116 @@ describe('PasswordField', () => {
92113
expect(passwordField.find('#number-check span').prop('className')).toEqual('pgn__icon text-success mr-1');
93114
expect(passwordField.find('#characters-check span').prop('className')).toEqual('pgn__icon text-success mr-1');
94115
});
116+
117+
it('should not run validations when blur is fired on password icon click', () => {
118+
const passwordField = mount(reduxWrapper(<IntlPasswordField {...props} />));
119+
120+
passwordField.find('button[aria-label="Show password"]').simulate('blur', {
121+
target: {
122+
name: 'password',
123+
value: 'invalid',
124+
},
125+
relatedTarget: {
126+
name: 'passwordIcon',
127+
},
128+
});
129+
130+
expect(passwordField.find('div[feedback-for="password"]').exists()).toBeFalsy();
131+
});
132+
133+
it('should call props handle blur if available', () => {
134+
props = {
135+
...props,
136+
handleBlur: jest.fn(),
137+
};
138+
const passwordField = mount(reduxWrapper(<IntlPasswordField {...props} />));
139+
140+
passwordField.find('input#password').simulate('blur', {
141+
target: {
142+
name: 'password',
143+
value: '',
144+
},
145+
});
146+
147+
expect(props.handleBlur).toHaveBeenCalledTimes(1);
148+
});
149+
150+
it('should run validations when blur event when rendered from register page', () => {
151+
props = {
152+
...props,
153+
handleErrorChange: jest.fn(),
154+
};
155+
const passwordField = mount(reduxWrapper(<IntlPasswordField {...props} />));
156+
157+
passwordField.find('input#password').simulate('blur', {
158+
target: {
159+
name: 'password',
160+
value: '',
161+
},
162+
});
163+
164+
expect(props.handleErrorChange).toHaveBeenCalledTimes(1);
165+
expect(props.handleErrorChange).toHaveBeenCalledWith(
166+
'password',
167+
'Password criteria has not been met',
168+
);
169+
});
170+
171+
it('should not clear error when focus is fired on password icon click when rendered from register page', () => {
172+
props = {
173+
...props,
174+
handleErrorChange: jest.fn(),
175+
};
176+
177+
const passwordField = mount(reduxWrapper(<IntlPasswordField {...props} />));
178+
179+
passwordField.find('button[aria-label="Show password"]').simulate('focus', {
180+
target: {
181+
name: 'passwordIcon',
182+
value: '',
183+
},
184+
});
185+
186+
expect(props.handleErrorChange).toHaveBeenCalledTimes(0);
187+
});
188+
189+
it('should clear error when focus is fired on password icon click when rendered from register page', () => {
190+
props = {
191+
...props,
192+
handleErrorChange: jest.fn(),
193+
};
194+
195+
const passwordField = mount(reduxWrapper(<IntlPasswordField {...props} />));
196+
197+
passwordField.find('button[aria-label="Show password"]').simulate('focus', {
198+
target: {
199+
name: 'password',
200+
value: 'invalid',
201+
},
202+
});
203+
204+
expect(props.handleErrorChange).toHaveBeenCalledTimes(1);
205+
expect(props.handleErrorChange).toHaveBeenCalledWith(
206+
'password',
207+
'',
208+
);
209+
});
210+
211+
it('should run backend validations when frontend validations pass on blur when rendered from register page', () => {
212+
store.dispatch = jest.fn(store.dispatch);
213+
props = {
214+
...props,
215+
handleErrorChange: jest.fn(),
216+
};
217+
const passwordField = mount(reduxWrapper(<IntlPasswordField {...props} />));
218+
219+
passwordField.find('button[aria-label="Show password"]').simulate('blur', {
220+
target: {
221+
name: 'password',
222+
value: 'password123',
223+
},
224+
});
225+
226+
expect(store.dispatch).toHaveBeenCalledWith(fetchRealtimeValidations({ password: 'password123' }));
227+
});
95228
});

src/data/constants.js

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,12 @@ export const FAILURE_STATE = 'failure';
2626
export const FORBIDDEN_STATE = 'forbidden';
2727
export const EMBEDDED = 'embedded';
2828

29-
// Regex
29+
export const LETTER_REGEX = /[a-zA-Z]/;
30+
export const NUMBER_REGEX = /\d/;
3031
export const VALID_EMAIL_REGEX = '(^[-!#$%&\'*+/=?^_`{}|~0-9A-Z]+(\\.[-!#$%&\'*+/=?^_`{}|~0-9A-Z]+)*'
3132
+ '|^"([\\001-\\010\\013\\014\\016-\\037!#-\\[\\]-\\177]|\\\\[\\001-\\011\\013\\014\\016-\\177])*"'
3233
+ ')@((?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\\.)+)(?:[A-Z0-9-]{2,63})'
3334
+ '|\\[(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\]$';
34-
export const LETTER_REGEX = /[a-zA-Z]/;
35-
export const NUMBER_REGEX = /\d/;
36-
export const INVALID_NAME_REGEX = /[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)?/gi; // eslint-disable-line no-useless-escape
3735

3836
// Query string parameters that can be passed to LMS to manage
3937
// things like auto-enrollment upon login and registration.

src/login/tests/LoginPage.test.jsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ describe('LoginPage', () => {
5555
secondaryProviders: [],
5656
},
5757
},
58+
register: {
59+
validationApiRateLimited: false,
60+
},
5861
};
5962

6063
const secondaryProviders = {

0 commit comments

Comments
 (0)