From c0cf4623a43a194fb3020e37b3a4d55049a24234 Mon Sep 17 00:00:00 2001 From: "Adolfo R. Brandes" Date: Fri, 13 Mar 2026 08:41:52 -0300 Subject: [PATCH] refactor: decouple PasswordField from RegisterContext via props PasswordField is a shared component used across login, registration, and reset-password flows, but it was reaching directly into RegisterContext for validation state and callbacks. Replace context coupling with explicit props (validateField, clearRegistrationBackendError, validationApiRateLimited) passed by RegistrationPage, and remove the now-unused useRegisterContextOptional hook. Co-Authored-By: Claude Opus 4.6 --- src/common-components/PasswordField.jsx | 25 +++++++---------- .../tests/FormField.test.jsx | 27 +++++-------------- src/register/RegistrationPage.jsx | 13 ++++++++- src/register/RegistrationPage.test.jsx | 1 - src/register/components/RegisterContext.tsx | 7 ----- .../ConfigurableRegistrationForm.test.jsx | 1 - .../tests/RegistrationFailure.test.jsx | 1 - .../components/tests/ThirdPartyAuth.test.jsx | 1 - 8 files changed, 27 insertions(+), 49 deletions(-) diff --git a/src/common-components/PasswordField.jsx b/src/common-components/PasswordField.jsx index 3f504ded..0a0be88d 100644 --- a/src/common-components/PasswordField.jsx +++ b/src/common-components/PasswordField.jsx @@ -10,8 +10,6 @@ import { import PropTypes from 'prop-types'; import { LETTER_REGEX, NUMBER_REGEX } from '../data/constants'; -import { useRegisterContextOptional } from '../register/components/RegisterContext'; -import { useFieldValidations } from '../register/data/apiHook'; import { validatePasswordField } from '../register/data/utils'; import messages from './messages'; @@ -22,22 +20,11 @@ const PasswordField = (props) => { const [isPasswordHidden, setHiddenTrue, setHiddenFalse] = useToggle(true); const [showTooltip, setShowTooltip] = useState(false); - const registerContext = useRegisterContextOptional(); const { - setValidationsSuccess = noopFn, - setValidationsFailure = noopFn, validationApiRateLimited = false, clearRegistrationBackendError = noopFn, - } = registerContext || {}; - - const fieldValidationsMutation = useFieldValidations({ - onSuccess: (data) => { - setValidationsSuccess(data); - }, - onError: () => { - setValidationsFailure(); - }, - }); + validateField = noopFn, + } = props; const handleBlur = (e) => { const { name, value } = e.target; @@ -66,7 +53,7 @@ const PasswordField = (props) => { if (fieldError) { props.handleErrorChange('password', fieldError); } else if (!validationApiRateLimited) { - fieldValidationsMutation.mutate({ password: passwordValue }); + validateField({ password: passwordValue }); } } }; @@ -171,6 +158,9 @@ PasswordField.defaultProps = { showRequirements: true, showScreenReaderText: true, autoComplete: null, + clearRegistrationBackendError: noopFn, + validateField: noopFn, + validationApiRateLimited: false, }; PasswordField.propTypes = { @@ -186,6 +176,9 @@ PasswordField.propTypes = { value: PropTypes.string.isRequired, autoComplete: PropTypes.string, showScreenReaderText: PropTypes.bool, + clearRegistrationBackendError: PropTypes.func, + validateField: PropTypes.func, + validationApiRateLimited: PropTypes.bool, }; export default PasswordField; diff --git a/src/common-components/tests/FormField.test.jsx b/src/common-components/tests/FormField.test.jsx index 468f7be3..3a207dc1 100644 --- a/src/common-components/tests/FormField.test.jsx +++ b/src/common-components/tests/FormField.test.jsx @@ -1,18 +1,10 @@ import { IntlProvider } from '@openedx/frontend-base'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { fireEvent, render } from '@testing-library/react'; import { act } from 'react-dom/test-utils'; -import { MemoryRouter } from 'react-router-dom'; import FormGroup from '../FormGroup'; import PasswordField from '../PasswordField'; -// Mock the register apiHook to prevent actual mutations -const mockFieldValidationsMutate = jest.fn(); -jest.mock('../../register/data/apiHook', () => ({ - useFieldValidations: () => ({ mutate: mockFieldValidationsMutate, isPending: false }), - useRegistration: () => ({ mutate: jest.fn(), isPending: false }), -})); describe('FormGroup', () => { const props = { @@ -40,23 +32,14 @@ describe('FormGroup', () => { describe('PasswordField', () => { let props = {}; - let queryClient; const wrapper = children => ( - - - - {children} - - - + + {children} + ); beforeEach(() => { - queryClient = new QueryClient({ - defaultOptions: { queries: { retry: false }, mutations: { retry: false } }, - }); - mockFieldValidationsMutate.mockClear(); props = { floatingLabel: 'Password', name: 'password', @@ -243,9 +226,11 @@ describe('PasswordField', () => { }); it('should run backend validations when frontend validations pass on blur when rendered from register page', () => { + const mockValidateField = jest.fn(); props = { ...props, handleErrorChange: jest.fn(), + validateField: mockValidateField, }; const { getByLabelText } = render(wrapper()); const passwordField = getByLabelText('Password'); @@ -256,7 +241,7 @@ describe('PasswordField', () => { }, }); - expect(mockFieldValidationsMutate).toHaveBeenCalledWith({ password: 'password123' }); + expect(mockValidateField).toHaveBeenCalledWith({ password: 'password123' }); }); it('should use password value from prop when password icon is focused out (blur due to icon)', () => { diff --git a/src/register/RegistrationPage.jsx b/src/register/RegistrationPage.jsx index 73560e43..9e3c88ba 100644 --- a/src/register/RegistrationPage.jsx +++ b/src/register/RegistrationPage.jsx @@ -15,7 +15,7 @@ import Skeleton from 'react-loading-skeleton'; import ConfigurableRegistrationForm from './components/ConfigurableRegistrationForm'; import { useRegisterContext } from './components/RegisterContext'; import RegistrationFailure from './components/RegistrationFailure'; -import { useRegistration } from './data/apiHook'; +import { useFieldValidations, useRegistration } from './data/apiHook'; import { FORM_SUBMISSION_ERROR, TPA_AUTHENTICATION_FAILURE, @@ -76,10 +76,18 @@ const RegistrationPage = (props) => { updateRegistrationFormData, setRegistrationError, setRegistrationResult, + setValidationsSuccess, + setValidationsFailure, + validationApiRateLimited, backendValidations, setBackendCountryCode, } = useRegisterContext(); + const fieldValidationsMutation = useFieldValidations({ + onSuccess: (data) => { setValidationsSuccess(data); }, + onError: () => { setValidationsFailure(); }, + }); + const registrationEmbedded = isHostAvailableInQueryParams(); const platformName = getSiteConfig().siteName; const { @@ -395,6 +403,9 @@ const RegistrationPage = (props) => { handleErrorChange={handleErrorChange} errorMessage={errors.password} floatingLabel={formatMessage(messages['registration.password.label'])} + clearRegistrationBackendError={clearRegistrationBackendError} + validateField={fieldValidationsMutation.mutate} + validationApiRateLimited={validationApiRateLimited} /> )} ({ jest.mock('./components/RegisterContext', () => ({ useRegisterContext: jest.fn(), - useRegisterContextOptional: jest.fn(), RegisterProvider: ({ children }) => children, })); diff --git a/src/register/components/RegisterContext.tsx b/src/register/components/RegisterContext.tsx index f00f3b79..7c254d46 100644 --- a/src/register/components/RegisterContext.tsx +++ b/src/register/components/RegisterContext.tsx @@ -213,10 +213,3 @@ export const useRegisterContext = () => { } return context; }; - -/** - * Optional version of useRegisterContext that returns null when outside a RegisterProvider. - * Useful for components like PasswordField that are shared across login, registration, - * and reset-password flows. - */ -export const useRegisterContextOptional = () => useContext(RegisterContext); diff --git a/src/register/components/tests/ConfigurableRegistrationForm.test.jsx b/src/register/components/tests/ConfigurableRegistrationForm.test.jsx index ebc05e96..7f4b3c23 100644 --- a/src/register/components/tests/ConfigurableRegistrationForm.test.jsx +++ b/src/register/components/tests/ConfigurableRegistrationForm.test.jsx @@ -33,7 +33,6 @@ jest.mock('../../data/apiHook', () => ({ jest.mock('../RegisterContext', () => ({ RegisterProvider: ({ children }) => children, useRegisterContext: jest.fn(), - useRegisterContextOptional: jest.fn(), })); jest.mock('../../../common-components/components/ThirdPartyAuthContext', () => ({ ThirdPartyAuthProvider: ({ children }) => children, diff --git a/src/register/components/tests/RegistrationFailure.test.jsx b/src/register/components/tests/RegistrationFailure.test.jsx index 63aefca0..3d98f73d 100644 --- a/src/register/components/tests/RegistrationFailure.test.jsx +++ b/src/register/components/tests/RegistrationFailure.test.jsx @@ -35,7 +35,6 @@ jest.mock('../../data/apiHook', () => ({ jest.mock('../RegisterContext', () => ({ RegisterProvider: ({ children }) => children, useRegisterContext: jest.fn(), - useRegisterContextOptional: jest.fn(), })); jest.mock('../../../common-components/components/ThirdPartyAuthContext', () => ({ ThirdPartyAuthProvider: ({ children }) => children, diff --git a/src/register/components/tests/ThirdPartyAuth.test.jsx b/src/register/components/tests/ThirdPartyAuth.test.jsx index 2a229a9f..8353abdc 100644 --- a/src/register/components/tests/ThirdPartyAuth.test.jsx +++ b/src/register/components/tests/ThirdPartyAuth.test.jsx @@ -34,7 +34,6 @@ jest.mock('../../data/apiHook', () => ({ jest.mock('../RegisterContext', () => ({ RegisterProvider: ({ children }) => children, useRegisterContext: jest.fn(), - useRegisterContextOptional: jest.fn(), })); jest.mock('../../../common-components/components/ThirdPartyAuthContext', () => ({ ThirdPartyAuthProvider: ({ children }) => children,