From df72db0e0b09b11ae150950ca0c36ab86c149cbd Mon Sep 17 00:00:00 2001 From: Chetan Agarwal Date: Tue, 27 Jan 2026 17:07:23 +0530 Subject: [PATCH 1/4] refactor(registration): reduce RegisterForm complexity by extracting subcomponents - Extract form validation logic into useRegisterFormValidation hook - Extract error handling logic into useRegisterErrorHandler hook - Create FormFieldInput component for reusable form fields - Create PasswordFieldWithVerifier component for password fields - Refactor RegisterForm to use new hooks and components - Remove eslint-disable complexity comment This refactoring reduces RegisterForm.tsx from 316 to 171 lines (-145 lines) and improves maintainability by separating concerns. --- .../web-ui-registration/src/RegisterForm.tsx | 265 ++++-------------- .../src/components/FormFieldInput.tsx | 49 ++++ .../components/PasswordFieldWithVerifier.tsx | 88 ++++++ .../src/hooks/useRegisterErrorHandler.ts | 66 +++++ .../src/hooks/useRegisterFormValidation.ts | 59 ++++ 5 files changed, 322 insertions(+), 205 deletions(-) create mode 100644 packages/web-ui-registration/src/components/FormFieldInput.tsx create mode 100644 packages/web-ui-registration/src/components/PasswordFieldWithVerifier.tsx create mode 100644 packages/web-ui-registration/src/hooks/useRegisterErrorHandler.ts create mode 100644 packages/web-ui-registration/src/hooks/useRegisterFormValidation.ts diff --git a/packages/web-ui-registration/src/RegisterForm.tsx b/packages/web-ui-registration/src/RegisterForm.tsx index 9b7f3e548d3eb..326a352ec5692 100644 --- a/packages/web-ui-registration/src/RegisterForm.tsx +++ b/packages/web-ui-registration/src/RegisterForm.tsx @@ -1,20 +1,7 @@ -/* eslint-disable complexity */ -import { - FieldGroup, - TextInput, - Field, - FieldLabel, - FieldRow, - FieldError, - PasswordInput, - ButtonGroup, - Button, - TextAreaInput, - Callout, -} from '@rocket.chat/fuselage'; +import { FieldGroup, ButtonGroup, Button, Callout } from '@rocket.chat/fuselage'; import { Form, ActionLink } from '@rocket.chat/layout'; -import { CustomFieldsForm, PasswordVerifier, useValidatePassword } from '@rocket.chat/ui-client'; -import { useAccountsCustomFields, useSetting, useToastMessageDispatch } from '@rocket.chat/ui-contexts'; +import { CustomFieldsForm } from '@rocket.chat/ui-client'; +import { useAccountsCustomFields, useSetting } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import { useEffect, useId, useRef, useState } from 'react'; import { useForm } from 'react-hook-form'; @@ -23,6 +10,10 @@ import { Trans, useTranslation } from 'react-i18next'; import EmailConfirmationForm from './EmailConfirmationForm'; import type { DispatchLoginRouter } from './hooks/useLoginRouter'; import { useRegisterMethod } from './hooks/useRegisterMethod'; +import { useRegisterFormValidation } from './hooks/useRegisterFormValidation'; +import { useRegisterErrorHandler } from './hooks/useRegisterErrorHandler'; +import { FormFieldInput } from './components/FormFieldInput'; +import { PasswordFieldWithVerifier } from './components/PasswordFieldWithVerifier'; type LoginRegisterPayload = { name: string; @@ -36,10 +27,6 @@ type LoginRegisterPayload = { export const RegisterForm = ({ setLoginRoute }: { setLoginRoute: DispatchLoginRouter }): ReactElement => { const { t } = useTranslation(); - const requireNameForRegister = useSetting('Accounts_RequireNameForSignUp', true); - const requiresPasswordConfirmation = useSetting('Accounts_RequirePasswordConfirmation', true); - const manuallyApproveNewUsersRequired = useSetting('Accounts_ManuallyApproveNewUsers', false); - const usernameOrEmailPlaceholder = useSetting('Accounts_EmailOrUsernamePlaceholder', ''); const passwordPlaceholder = useSetting('Accounts_PasswordPlaceholder', ''); const passwordConfirmationPlaceholder = useSetting('Accounts_ConfirmPasswordPlaceholder', ''); @@ -58,8 +45,6 @@ export const RegisterForm = ({ setLoginRoute }: { setLoginRoute: DispatchLoginRo const [serverError, setServerError] = useState(undefined); - const dispatchToastMessage = useToastMessageDispatch(); - const { register, handleSubmit, @@ -71,8 +56,10 @@ export const RegisterForm = ({ setLoginRoute }: { setLoginRoute: DispatchLoginRo formState: { errors }, } = useForm({ mode: 'onBlur' }); - const { password } = watch(); - const passwordIsValid = useValidatePassword(password); + const { validationRules, requireNameForRegister, requiresPasswordConfirmation, manuallyApproveNewUsersRequired, passwordIsValid } = + useRegisterFormValidation(watch); + + const { handleRegisterError } = useRegisterErrorHandler(setError, setServerError, setLoginRoute); const registerFormRef = useRef(null); @@ -86,39 +73,7 @@ export const RegisterForm = ({ setLoginRoute }: { setLoginRoute: DispatchLoginRo registerUser.mutate( { pass: password, ...formData }, { - onError: (error: any) => { - if ([error.error, error.errorType].includes('error-invalid-email')) { - setError('email', { type: 'invalid-email', message: t('registration.component.form.invalidEmail') }); - } - if (error.errorType === 'error-user-already-exists') { - setError('username', { type: 'user-already-exists', message: t('registration.component.form.usernameAlreadyExists') }); - } - if (/Email already exists/.test(error.error)) { - setError('email', { type: 'email-already-exists', message: t('registration.component.form.emailAlreadyExists') }); - } - if (/Username is already in use/.test(error.error)) { - setError('username', { type: 'username-already-exists', message: t('registration.component.form.userAlreadyExist') }); - } - if (/The username provided is not valid/.test(error.error)) { - setError('username', { - type: 'username-contains-invalid-chars', - message: t('registration.component.form.usernameContainsInvalidChars'), - }); - } - if (/Name contains invalid characters/.test(error.error)) { - setError('name', { type: 'name-contains-invalid-chars', message: t('registration.component.form.nameContainsInvalidChars') }); - } - if (/error-too-many-requests/.test(error.error)) { - dispatchToastMessage({ type: 'error', message: error.error }); - } - if (/error-user-is-not-activated/.test(error.error)) { - dispatchToastMessage({ type: 'info', message: t('registration.page.registration.waitActivationWarning') }); - setLoginRoute('login'); - } - if (error.error === 'error-user-registration-custom-field') { - setServerError(error.message); - } - }, + onError: handleRegisterError, }, ); }; @@ -140,155 +95,55 @@ export const RegisterForm = ({ setLoginRoute }: { setLoginRoute: DispatchLoginRo - - - {t('registration.component.form.name')} - - - - - {errors.name && ( - - {errors.name.message} - - )} - - - - {t('registration.component.form.email')} - - - - - {errors.email && ( - - {errors.email.message} - - )} - - - - {t('registration.component.form.username')} - - - - - {errors.username && ( - - {errors.username.message} - - )} - - - - {t('registration.component.form.password')} - - - (!passwordIsValid ? t('Password_must_meet_the_complexity_requirements') : true), - })} - error={errors.password?.message} - aria-required='true' - aria-invalid={errors.password ? 'true' : undefined} - id={passwordId} - placeholder={passwordPlaceholder || t('Create_a_password')} - aria-describedby={`${passwordVerifierId} ${passwordId}-error`} - /> - - {errors?.password && ( - - {errors.password.message} - - )} - - - {requiresPasswordConfirmation && ( - - - {t('registration.component.form.confirmPassword')} - - - (watch('password') === val ? true : t('registration.component.form.invalidConfirmPass')), - })} - error={errors.passwordConfirmation?.message} - aria-required='true' - aria-invalid={errors.passwordConfirmation ? 'true' : 'false'} - id={passwordConfirmationId} - aria-describedby={`${passwordConfirmationId}-error`} - placeholder={passwordConfirmationPlaceholder || t('Confirm_password')} - disabled={!passwordIsValid} - /> - - {errors.passwordConfirmation && ( - - {errors.passwordConfirmation.message} - - )} - - )} + + + + {manuallyApproveNewUsersRequired && ( - - - {t('registration.component.form.reasonToJoin')} - - - - - {errors.reason && ( - - {errors.reason.message} - - )} - + )} {serverError && {serverError}} diff --git a/packages/web-ui-registration/src/components/FormFieldInput.tsx b/packages/web-ui-registration/src/components/FormFieldInput.tsx new file mode 100644 index 0000000000000..2fc1a61edebdf --- /dev/null +++ b/packages/web-ui-registration/src/components/FormFieldInput.tsx @@ -0,0 +1,49 @@ +import { Field, FieldLabel, FieldRow, FieldError, TextInput, TextAreaInput } from '@rocket.chat/fuselage'; +import type { ComponentProps, ReactElement } from 'react'; +import type { FieldError as FieldErrorType, UseFormRegisterReturn } from 'react-hook-form'; + +type FormFieldInputProps = { + label: string; + fieldId: string; + required?: boolean; + error?: FieldErrorType; + placeholder?: string; + register: UseFormRegisterReturn; + type?: 'text' | 'textarea'; +}; + +export const FormFieldInput = ({ + label, + fieldId, + required = false, + error, + placeholder, + register, + type = 'text', +}: FormFieldInputProps): ReactElement => { + const InputComponent = type === 'textarea' ? TextAreaInput : TextInput; + + return ( + + + {label} + + + + + {error && ( + + {error.message} + + )} + + ); +}; diff --git a/packages/web-ui-registration/src/components/PasswordFieldWithVerifier.tsx b/packages/web-ui-registration/src/components/PasswordFieldWithVerifier.tsx new file mode 100644 index 0000000000000..fe39c54250186 --- /dev/null +++ b/packages/web-ui-registration/src/components/PasswordFieldWithVerifier.tsx @@ -0,0 +1,88 @@ +import { Field, FieldLabel, FieldRow, FieldError, PasswordInput } from '@rocket.chat/fuselage'; +import { PasswordVerifier } from '@rocket.chat/ui-client'; +import type { ReactElement } from 'react'; +import type { FieldError as FieldErrorType, UseFormRegisterReturn } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; + +type PasswordFieldWithVerifierProps = { + passwordId: string; + passwordVerifierId: string; + passwordConfirmationId?: string; + passwordError?: FieldErrorType; + passwordConfirmationError?: FieldErrorType; + passwordRegister: UseFormRegisterReturn; + passwordConfirmationRegister?: UseFormRegisterReturn; + password: string; + passwordIsValid: boolean; + requiresPasswordConfirmation: boolean; + passwordPlaceholder?: string; + passwordConfirmationPlaceholder?: string; +}; + +export const PasswordFieldWithVerifier = ({ + passwordId, + passwordVerifierId, + passwordConfirmationId, + passwordError, + passwordConfirmationError, + passwordRegister, + passwordConfirmationRegister, + password, + passwordIsValid, + requiresPasswordConfirmation, + passwordPlaceholder, + passwordConfirmationPlaceholder, +}: PasswordFieldWithVerifierProps): ReactElement => { + const { t } = useTranslation(); + + return ( + <> + + + {t('registration.component.form.password')} + + + + + {passwordError && ( + + {passwordError.message} + + )} + + + {requiresPasswordConfirmation && passwordConfirmationId && passwordConfirmationRegister && ( + + + {t('registration.component.form.confirmPassword')} + + + + + {passwordConfirmationError && ( + + {passwordConfirmationError.message} + + )} + + )} + + ); +}; diff --git a/packages/web-ui-registration/src/hooks/useRegisterErrorHandler.ts b/packages/web-ui-registration/src/hooks/useRegisterErrorHandler.ts new file mode 100644 index 0000000000000..5677e84074f0e --- /dev/null +++ b/packages/web-ui-registration/src/hooks/useRegisterErrorHandler.ts @@ -0,0 +1,66 @@ +import type { UseFormSetError } from 'react-hook-form'; +import { useToastMessageDispatch } from '@rocket.chat/ui-contexts'; +import { useTranslation } from 'react-i18next'; +import type { DispatchLoginRouter } from './useLoginRouter'; + +type LoginRegisterPayload = { + name: string; + passwordConfirmation: string; + username: string; + password: string; + email: string; + reason: string; +}; + +export const useRegisterErrorHandler = ( + setError: UseFormSetError, + setServerError: (error: string | undefined) => void, + setLoginRoute: DispatchLoginRouter, +) => { + const { t } = useTranslation(); + const dispatchToastMessage = useToastMessageDispatch(); + + const handleRegisterError = (error: any) => { + if ([error.error, error.errorType].includes('error-invalid-email')) { + setError('email', { type: 'invalid-email', message: t('registration.component.form.invalidEmail') }); + } + + if (error.errorType === 'error-user-already-exists') { + setError('username', { type: 'user-already-exists', message: t('registration.component.form.usernameAlreadyExists') }); + } + + if (/Email already exists/.test(error.error)) { + setError('email', { type: 'email-already-exists', message: t('registration.component.form.emailAlreadyExists') }); + } + + if (/Username is already in use/.test(error.error)) { + setError('username', { type: 'username-already-exists', message: t('registration.component.form.userAlreadyExist') }); + } + + if (/The username provided is not valid/.test(error.error)) { + setError('username', { + type: 'username-contains-invalid-chars', + message: t('registration.component.form.usernameContainsInvalidChars'), + }); + } + + if (/Name contains invalid characters/.test(error.error)) { + setError('name', { type: 'name-contains-invalid-chars', message: t('registration.component.form.nameContainsInvalidChars') }); + } + + if (/error-too-many-requests/.test(error.error)) { + dispatchToastMessage({ type: 'error', message: error.error }); + } + + if (/error-user-is-not-activated/.test(error.error)) { + dispatchToastMessage({ type: 'info', message: t('registration.page.registration.waitActivationWarning') }); + setLoginRoute('login'); + } + + if (error.error === 'error-user-registration-custom-field') { + setServerError(error.message); + } + }; + + return { handleRegisterError }; +}; diff --git a/packages/web-ui-registration/src/hooks/useRegisterFormValidation.ts b/packages/web-ui-registration/src/hooks/useRegisterFormValidation.ts new file mode 100644 index 0000000000000..6f2214482ebc0 --- /dev/null +++ b/packages/web-ui-registration/src/hooks/useRegisterFormValidation.ts @@ -0,0 +1,59 @@ +import { useSetting } from '@rocket.chat/ui-contexts'; +import { useValidatePassword } from '@rocket.chat/ui-client'; +import type { UseFormWatch } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; + +type LoginRegisterPayload = { + name: string; + passwordConfirmation: string; + username: string; + password: string; + email: string; + reason: string; +}; + +export const useRegisterFormValidation = (watch: UseFormWatch) => { + const { t } = useTranslation(); + const requireNameForRegister = useSetting('Accounts_RequireNameForSignUp', true); + const requiresPasswordConfirmation = useSetting('Accounts_RequirePasswordConfirmation', true); + const manuallyApproveNewUsersRequired = useSetting('Accounts_ManuallyApproveNewUsers', false); + + const { password } = watch(); + const passwordIsValid = useValidatePassword(password); + + const validationRules = { + name: { + required: requireNameForRegister ? t('Required_field', { field: t('registration.component.form.name') }) : false, + }, + email: { + required: t('Required_field', { field: t('registration.component.form.email') }), + pattern: { + value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i, + message: t('registration.component.form.invalidEmail'), + }, + }, + username: { + required: t('Required_field', { field: t('registration.component.form.username') }), + }, + password: { + required: t('Required_field', { field: t('registration.component.form.password') }), + validate: () => (!passwordIsValid ? t('Password_must_meet_the_complexity_requirements') : true), + }, + passwordConfirmation: { + required: t('Required_field', { field: t('registration.component.form.confirmPassword') }), + deps: ['password'] as const, + validate: (val: string) => (watch('password') === val ? true : t('registration.component.form.invalidConfirmPass')), + }, + reason: { + required: t('Required_field', { field: t('registration.component.form.reasonToJoin') }), + }, + }; + + return { + validationRules, + requireNameForRegister, + requiresPasswordConfirmation, + manuallyApproveNewUsersRequired, + passwordIsValid, + }; +}; From 601b27e55b25b247a5ff765b8efb9f8f8b4d0e41 Mon Sep 17 00:00:00 2001 From: Chetan Agarwal Date: Wed, 28 Jan 2026 00:18:48 +0530 Subject: [PATCH 2/4] test(registration): add comprehensive test coverage for RegisterForm refactoring - Add RegisterForm.spec.tsx with 26+ tests covering: * Rendering all required and conditional fields * Validation for all form fields * Password confirmation matching * Form submission behavior * Accessibility attributes - Add useRegisterFormValidation.spec.tsx with 7 tests covering: * Validation rules generation * Settings-based conditional requirements * Password matching logic - Add useRegisterErrorHandler.spec.tsx with 9 tests covering: * All error scenarios (invalid email, duplicate username, etc.) * Toast message dispatching * Login route redirects - Add FormFieldInput.spec.tsx with 10 tests covering: * Text and textarea rendering * Required field handling * Error display and accessibility - Add PasswordFieldWithVerifier.spec.tsx with 10 tests covering: * Password and confirmation field rendering * Conditional confirmation field * Password validation state * Error handling All tests use @testing-library/react and mockAppRoot for proper isolation and follow existing project testing patterns. --- .../src/RegisterForm.spec.tsx | 368 ++++++++++++++++++ .../src/components/FormFieldInput.spec.tsx | 118 ++++++ .../PasswordFieldWithVerifier.spec.tsx | 210 ++++++++++ .../hooks/useRegisterErrorHandler.spec.tsx | 160 ++++++++ .../hooks/useRegisterFormValidation.spec.tsx | 114 ++++++ 5 files changed, 970 insertions(+) create mode 100644 packages/web-ui-registration/src/RegisterForm.spec.tsx create mode 100644 packages/web-ui-registration/src/components/FormFieldInput.spec.tsx create mode 100644 packages/web-ui-registration/src/components/PasswordFieldWithVerifier.spec.tsx create mode 100644 packages/web-ui-registration/src/hooks/useRegisterErrorHandler.spec.tsx create mode 100644 packages/web-ui-registration/src/hooks/useRegisterFormValidation.spec.tsx diff --git a/packages/web-ui-registration/src/RegisterForm.spec.tsx b/packages/web-ui-registration/src/RegisterForm.spec.tsx new file mode 100644 index 0000000000000..3c53de3c0709b --- /dev/null +++ b/packages/web-ui-registration/src/RegisterForm.spec.tsx @@ -0,0 +1,368 @@ +import { mockAppRoot } from '@rocket.chat/mock-providers'; +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { RegisterForm } from './RegisterForm'; + +const mockSetLoginRoute = jest.fn(); +const mockRegisterMethod = jest.fn(); + +// Mock the useRegisterMethod hook +jest.mock('./hooks/useRegisterMethod', () => ({ + useRegisterMethod: () => ({ + mutate: mockRegisterMethod, + isPending: false, + }), +})); + +const defaultAppRoot = mockAppRoot() + .withTranslations('en', 'core', { + Required_field: '{{field}} is required', + 'registration.component.form.createAnAccount': 'Create an account', + 'registration.component.form.name': 'Name', + 'registration.component.form.email': 'Email', + 'registration.component.form.username': 'Username', + 'registration.component.form.password': 'Password', + 'registration.component.form.confirmPassword': 'Confirm password', + 'registration.component.form.reasonToJoin': 'Reason to join', + 'registration.component.form.joinYourTeam': 'Join your team', + 'registration.component.form.emailPlaceholder': 'Enter your email', + 'registration.component.form.invalidEmail': 'Invalid email format', + 'registration.component.form.invalidConfirmPass': "Passwords don't match", + 'onboarding.form.adminInfoForm.fields.fullName.placeholder': 'Enter your full name', + Create_a_password: 'Create a password', + Confirm_password: 'Confirm your password', + Password_must_meet_the_complexity_requirements: 'Password must meet complexity requirements', + 'registration.page.register.back': 'Back to Login', + }) + .withSetting('Accounts_RequireNameForSignUp', true) + .withSetting('Accounts_RequirePasswordConfirmation', true) + .withSetting('Accounts_ManuallyApproveNewUsers', false) + .withSetting('Accounts_EmailOrUsernamePlaceholder', '') + .withSetting('Accounts_PasswordPlaceholder', '') + .withSetting('Accounts_ConfirmPasswordPlaceholder', '') + .withSetting('Accounts_Password_Policy_Enabled', false) + .build(); + +describe('RegisterForm', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('Rendering', () => { + it('should render all required fields', () => { + render(, { wrapper: defaultAppRoot }); + + expect(screen.getByRole('textbox', { name: /name/i })).toBeInTheDocument(); + expect(screen.getByRole('textbox', { name: /email/i })).toBeInTheDocument(); + expect(screen.getByRole('textbox', { name: /username/i })).toBeInTheDocument(); + expect(screen.getByLabelText(/^password/i)).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /join your team/i })).toBeInTheDocument(); + }); + + it('should render form title', () => { + render(, { wrapper: defaultAppRoot }); + + expect(screen.getByText('Create an account')).toBeInTheDocument(); + }); + + it('should render back to login link', () => { + render(, { wrapper: defaultAppRoot }); + + expect(screen.getByText(/back to login/i)).toBeInTheDocument(); + }); + }); + + describe('Conditional Fields', () => { + it('should render name field as optional when not required', () => { + const appRoot = mockAppRoot() + .withTranslations('en', 'core', { + 'registration.component.form.name': 'Name', + }) + .withSetting('Accounts_RequireNameForSignUp', false) + .build(); + + render(, { wrapper: appRoot }); + + const nameField = screen.getByRole('textbox', { name: /name/i }); + expect(nameField).toBeInTheDocument(); + expect(nameField).not.toHaveAttribute('aria-required', 'true'); + }); + + it('should render password confirmation field when enabled', () => { + const appRoot = mockAppRoot() + .withTranslations('en', 'core', { + 'registration.component.form.confirmPassword': 'Confirm password', + }) + .withSetting('Accounts_RequirePasswordConfirmation', true) + .withSetting('Accounts_Password_Policy_Enabled', false) + .build(); + + render(, { wrapper: appRoot }); + + expect(screen.getByLabelText(/confirm password/i)).toBeInTheDocument(); + }); + + it('should NOT render password confirmation field when disabled', () => { + const appRoot = mockAppRoot() + .withTranslations('en', 'core', { + 'registration.component.form.password': 'Password', + }) + .withSetting('Accounts_RequirePasswordConfirmation', false) + .withSetting('Accounts_Password_Policy_Enabled', false) + .build(); + + render(, { wrapper: appRoot }); + + expect(screen.queryByLabelText(/confirm password/i)).not.toBeInTheDocument(); + }); + + it('should render reason field when manual approval is required', () => { + const appRoot = mockAppRoot() + .withTranslations('en', 'core', { + 'registration.component.form.reasonToJoin': 'Reason to join', + }) + .withSetting('Accounts_ManuallyApproveNewUsers', true) + .build(); + + render(, { wrapper: appRoot }); + + expect(screen.getByLabelText(/reason to join/i)).toBeInTheDocument(); + }); + + it('should NOT render reason field when manual approval is not required', () => { + const appRoot = mockAppRoot() + .withSetting('Accounts_ManuallyApproveNewUsers', false) + .build(); + + render(, { wrapper: appRoot }); + + expect(screen.queryByLabelText(/reason to join/i)).not.toBeInTheDocument(); + }); + }); + + describe('Validation', () => { + it('should show required error for name field', async () => { + const appRoot = mockAppRoot() + .withTranslations('en', 'core', { + Required_field: '{{field}} is required', + 'registration.component.form.name': 'Name', + }) + .withSetting('Accounts_RequireNameForSignUp', true) + .build(); + + render(, { wrapper: appRoot }); + + const nameInput = screen.getByRole('textbox', { name: /name/i }); + await userEvent.click(nameInput); + await userEvent.tab(); + + await waitFor(() => { + expect(nameInput).toHaveAccessibleDescription(/name is required/i); + }); + }); + + it('should show required error for email field', async () => { + const appRoot = mockAppRoot() + .withTranslations('en', 'core', { + Required_field: '{{field}} is required', + 'registration.component.form.email': 'Email', + }) + .build(); + + render(, { wrapper: appRoot }); + + const emailInput = screen.getByRole('textbox', { name: /email/i }); + await userEvent.click(emailInput); + await userEvent.tab(); + + await waitFor(() => { + expect(emailInput).toHaveAccessibleDescription(/email is required/i); + }); + }); + + it('should show invalid email format error', async () => { + const appRoot = mockAppRoot() + .withTranslations('en', 'core', { + 'registration.component.form.invalidEmail': 'Invalid email format', + }) + .build(); + + render(, { wrapper: appRoot }); + + const emailInput = screen.getByRole('textbox', { name: /email/i }); + await userEvent.type(emailInput, 'invalid-email'); + await userEvent.tab(); + + await waitFor(() => { + expect(emailInput).toHaveAccessibleDescription(/invalid email format/i); + }); + }); + + it('should accept valid email format', async () => { + render(, { wrapper: defaultAppRoot }); + + const emailInput = screen.getByRole('textbox', { name: /email/i }); + await userEvent.type(emailInput, 'test@example.com'); + await userEvent.tab(); + + await waitFor(() => { + expect(emailInput).not.toHaveAccessibleDescription(); + }); + }); + + it('should show required error for username field', async () => { + const appRoot = mockAppRoot() + .withTranslations('en', 'core', { + Required_field: '{{field}} is required', + 'registration.component.form.username': 'Username', + }) + .build(); + + render(, { wrapper: appRoot }); + + const usernameInput = screen.getByRole('textbox', { name: /username/i }); + await userEvent.click(usernameInput); + await userEvent.tab(); + + await waitFor(() => { + expect(usernameInput).toHaveAccessibleDescription(/username is required/i); + }); + }); + + it('should show password mismatch error', async () => { + const appRoot = mockAppRoot() + .withTranslations('en', 'core', { + 'registration.component.form.password': 'Password', + 'registration.component.form.confirmPassword': 'Confirm password', + 'registration.component.form.invalidConfirmPass': "Passwords don't match", + }) + .withSetting('Accounts_RequirePasswordConfirmation', true) + .withSetting('Accounts_Password_Policy_Enabled', false) + .build(); + + render(, { wrapper: appRoot }); + + const passwordInput = screen.getByLabelText(/^password/i); + const confirmPasswordInput = screen.getByLabelText(/confirm password/i); + + await userEvent.type(passwordInput, 'password123'); + await userEvent.type(confirmPasswordInput, 'password456'); + await userEvent.tab(); + + await waitFor(() => { + expect(confirmPasswordInput).toHaveAccessibleDescription(/passwords don't match/i); + }); + }); + }); + + describe('Form Submission', () => { + it('should call register mutation with correct data on valid submission', async () => { + const appRoot = mockAppRoot() + .withTranslations('en', 'core', { + 'registration.component.form.name': 'Name', + 'registration.component.form.email': 'Email', + 'registration.component.form.username': 'Username', + 'registration.component.form.password': 'Password', + 'registration.component.form.joinYourTeam': 'Join your team', + 'onboarding.form.adminInfoForm.fields.fullName.placeholder': 'Full Name', + }) + .withSetting('Accounts_RequirePasswordConfirmation', false) + .withSetting('Accounts_Password_Policy_Enabled', false) + .build(); + + render(, { wrapper: appRoot }); + + // Fill in the form + await userEvent.type(screen.getByRole('textbox', { name: /name/i }), 'John Doe'); + await userEvent.type(screen.getByRole('textbox', { name: /email/i }), 'john@example.com'); + await userEvent.type(screen.getByRole('textbox', { name: /username/i }), 'johndoe'); + await userEvent.type(screen.getByLabelText(/password/i), 'password123'); + + // Submit the form + await userEvent.click(screen.getByRole('button', { name: /join your team/i })); + + await waitFor(() => { + expect(mockRegisterMethod).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'John Doe', + email: 'john@example.com', + username: 'johndoe', + pass: 'password123', + }), + expect.any(Object), + ); + }); + }); + + it('should not include passwordConfirmation in submitted data', async () => { + const appRoot = mockAppRoot() + .withTranslations('en', 'core', { + 'registration.component.form.password': 'Password', + 'registration.component.form.confirmPassword': 'Confirm password', + }) + .withSetting('Accounts_RequirePasswordConfirmation', true) + .withSetting('Accounts_Password_Policy_Enabled', false) + .build(); + + render(, { wrapper: appRoot }); + + await userEvent.type(screen.getByRole('textbox', { name: /name/i }), 'John Doe'); + await userEvent.type(screen.getByRole('textbox', { name: /email/i }), 'john@example.com'); + await userEvent.type(screen.getByRole('textbox', { name: /username/i }), 'johndoe'); + await userEvent.type(screen.getByLabelText(/^password/i), 'password123'); + await userEvent.type(screen.getByLabelText(/confirm password/i), 'password123'); + + await userEvent.click(screen.getByRole('button', { name: /join your team/i })); + + await waitFor(() => { + expect(mockRegisterMethod).toHaveBeenCalled(); + const callArgs = mockRegisterMethod.mock.calls[0][0]; + expect(callArgs).not.toHaveProperty('passwordConfirmation'); + expect(callArgs).toHaveProperty('pass', 'password123'); + }); + }); + }); + + describe('Accessibility', () => { + it('should have proper ARIA labels', () => { + render(, { wrapper: defaultAppRoot }); + + const form = screen.getByRole('form'); + expect(form).toHaveAttribute('aria-labelledby'); + expect(form).toHaveAttribute('aria-describedby', 'welcomeTitle'); + }); + + it('should have proper ARIA attributes on input fields', () => { + render(, { wrapper: defaultAppRoot }); + + const emailInput = screen.getByRole('textbox', { name: /email/i }); + expect(emailInput).toHaveAttribute('aria-required', 'true'); + expect(emailInput).toHaveAttribute('aria-describedby'); + }); + }); + + describe('Back to Login', () => { + it('should call setLoginRoute when back to login is clicked', async () => { + render(, { wrapper: defaultAppRoot }); + + const backLink = screen.getByText(/back to login/i); + await userEvent.click(backLink); + + expect(mockSetLoginRoute).toHaveBeenCalledWith('login'); + }); + }); + + describe('Custom Fields Integration', () => { + it('should render custom fields form', () => { + // The CustomFieldsForm is already tested in ui-client + // We just verify it's rendered in RegisterForm + render(, { wrapper: defaultAppRoot }); + + // CustomFieldsForm component should be in the document + // This is an integration test to ensure the component is properly included + const form = screen.getByRole('form'); + expect(form).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/web-ui-registration/src/components/FormFieldInput.spec.tsx b/packages/web-ui-registration/src/components/FormFieldInput.spec.tsx new file mode 100644 index 0000000000000..4b11b6df38ccf --- /dev/null +++ b/packages/web-ui-registration/src/components/FormFieldInput.spec.tsx @@ -0,0 +1,118 @@ +import { mockAppRoot } from '@rocket.chat/mock-providers'; +import { render, screen } from '@testing-library/react'; + +import { FormFieldInput } from './FormFieldInput'; + +const mockRegister = { + onChange: jest.fn(), + onBlur: jest.fn(), + ref: jest.fn(), + name: 'testField', +}; + +const defaultAppRoot = mockAppRoot().build(); + +describe('FormFieldInput', () => { + it('should render text input with label', () => { + render( + , + { wrapper: defaultAppRoot }, + ); + + expect(screen.getByLabelText('Test Field')).toBeInTheDocument(); + expect(screen.getByRole('textbox')).toBeInTheDocument(); + }); + + it('should render textarea when type is textarea', () => { + render( + , + { wrapper: defaultAppRoot }, + ); + + const textarea = screen.getByLabelText('Test Field'); + expect(textarea.tagName.toLowerCase()).toBe('textarea'); + }); + + it('should mark field as required', () => { + render( + , + { wrapper: defaultAppRoot }, + ); + + const input = screen.getByRole('textbox'); + expect(input).toHaveAttribute('aria-required', 'true'); + }); + + it('should not mark field as required when not specified', () => { + render( + , + { wrapper: defaultAppRoot }, + ); + + const input = screen.getByRole('textbox'); + expect(input).toHaveAttribute('aria-required', 'false'); + }); + + it('should display error message', () => { + const error = { type: 'required', message: 'This field is required' }; + + render( + , + { wrapper: defaultAppRoot }, + ); + + expect(screen.getByText('This field is required')).toBeInTheDocument(); + expect(screen.getByRole('alert')).toBeInTheDocument(); + }); + + it('should not display error when no error provided', () => { + render( + , + { wrapper: defaultAppRoot }, + ); + + expect(screen.queryByRole('alert')).not.toBeInTheDocument(); + }); + + it('should set aria-invalid to true when error exists', () => { + const error = { type: 'required', message: 'Error' }; + + render( + , + { wrapper: defaultAppRoot }, + ); + + expect(screen.getByRole('textbox')).toHaveAttribute('aria-invalid', 'true'); + }); + + it('should set aria-invalid to false when no error', () => { + render( + , + { wrapper: defaultAppRoot }, + ); + + expect(screen.getByRole('textbox')).toHaveAttribute('aria-invalid', 'false'); + }); + + it('should apply placeholder', () => { + render( + , + { wrapper: defaultAppRoot }, + ); + + expect(screen.getByPlaceholderText('Enter text')).toBeInTheDocument(); + }); + + it('should have proper aria-describedby for error', () => { + const error = { type: 'required', message: 'Error' }; + + render( + , + { wrapper: defaultAppRoot }, + ); + + const input = screen.getByRole('textbox'); + expect(input).toHaveAttribute('aria-describedby', 'test-id-error'); + expect(screen.getByRole('alert')).toHaveAttribute('id', 'test-id-error'); + }); +}); diff --git a/packages/web-ui-registration/src/components/PasswordFieldWithVerifier.spec.tsx b/packages/web-ui-registration/src/components/PasswordFieldWithVerifier.spec.tsx new file mode 100644 index 0000000000000..123b073734aed --- /dev/null +++ b/packages/web-ui-registration/src/components/PasswordFieldWithVerifier.spec.tsx @@ -0,0 +1,210 @@ +import { mockAppRoot } from '@rocket.chat/mock-providers'; +import { render, screen } from '@testing-library/react'; + +import { PasswordFieldWithVerifier } from './PasswordFieldWithVerifier'; + +const mockPasswordRegister = { + onChange: jest.fn(), + onBlur: jest.fn(), + ref: jest.fn(), + name: 'password', +}; + +const mockConfirmRegister = { + onChange: jest.fn(), + onBlur: jest.fn(), + ref: jest.fn(), + name: 'passwordConfirmation', +}; + +const defaultAppRoot = mockAppRoot() + .withTranslations('en', 'core', { + 'registration.component.form.password': 'Password', + 'registration.component.form.confirmPassword': 'Confirm password', + Create_a_password: 'Create a password', + Confirm_password: 'Confirm your password', + }) + .withSetting('Accounts_Password_Policy_Enabled', false) + .build(); + +describe('PasswordFieldWithVerifier', () => { + it('should render password field', () => { + render( + , + { wrapper: defaultAppRoot }, + ); + + expect(screen.getByLabelText(/^password/i)).toBeInTheDocument(); + }); + + it('should render password confirmation when required', () => { + render( + , + { wrapper: defaultAppRoot }, + ); + + expect(screen.getByLabelText(/confirm password/i)).toBeInTheDocument(); + }); + + it('should NOT render password confirmation when not required', () => { + render( + , + { wrapper: defaultAppRoot }, + ); + + expect(screen.queryByLabelText(/confirm password/i)).not.toBeInTheDocument(); + }); + + it('should disable confirmation field when password is invalid', () => { + render( + , + { wrapper: defaultAppRoot }, + ); + + expect(screen.getByLabelText(/confirm password/i)).toBeDisabled(); + }); + + it('should enable confirmation field when password is valid', () => { + render( + , + { wrapper: defaultAppRoot }, + ); + + expect(screen.getByLabelText(/confirm password/i)).not.toBeDisabled(); + }); + + it('should display password error', () => { + const error = { type: 'required', message: 'Password is required' }; + + render( + , + { wrapper: defaultAppRoot }, + ); + + expect(screen.getByText('Password is required')).toBeInTheDocument(); + }); + + it('should display password confirmation error', () => { + const error = { type: 'validate', message: "Passwords don't match" }; + + render( + , + { wrapper: defaultAppRoot }, + ); + + expect(screen.getByText("Passwords don't match")).toBeInTheDocument(); + }); + + it('should apply password placeholder', () => { + render( + , + { wrapper: defaultAppRoot }, + ); + + expect(screen.getByPlaceholderText('Enter strong password')).toBeInTheDocument(); + }); + + it('should apply confirmation placeholder', () => { + render( + , + { wrapper: defaultAppRoot }, + ); + + expect(screen.getByPlaceholderText('Re-enter password')).toBeInTheDocument(); + }); + + it('should have proper aria attributes', () => { + render( + , + { wrapper: defaultAppRoot }, + ); + + const passwordInput = screen.getByLabelText(/^password/i); + expect(passwordInput).toHaveAttribute('aria-required', 'true'); + expect(passwordInput).toHaveAttribute('aria-describedby'); + }); +}); diff --git a/packages/web-ui-registration/src/hooks/useRegisterErrorHandler.spec.tsx b/packages/web-ui-registration/src/hooks/useRegisterErrorHandler.spec.tsx new file mode 100644 index 0000000000000..b68ede23bde43 --- /dev/null +++ b/packages/web-ui-registration/src/hooks/useRegisterErrorHandler.spec.tsx @@ -0,0 +1,160 @@ +import { renderHook } from '@testing-library/react'; +import { mockAppRoot } from '@rocket.chat/mock-providers'; + +import { useRegisterErrorHandler } from './useRegisterErrorHandler'; + +const mockSetError = jest.fn(); +const mockSetServerError = jest.fn(); +const mockSetLoginRoute = jest.fn(); +const mockDispatchToastMessage = jest.fn(); + +// Mock the useToastMessageDispatch hook +jest.mock('@rocket.chat/ui-contexts', () => ({ + ...jest.requireActual('@rocket.chat/ui-contexts'), + useToastMessageDispatch: () => mockDispatchToastMessage, +})); + +const defaultAppRoot = mockAppRoot() + .withTranslations('en', 'core', { + 'registration.component.form.invalidEmail': 'Invalid email format', + 'registration.component.form.usernameAlreadyExists': 'Username already exists', + 'registration.component.form.emailAlreadyExists': 'Email already exists', + 'registration.component.form.userAlreadyExist': 'User already exists', + 'registration.component.form.usernameContainsInvalidChars': 'Username contains invalid characters', + 'registration.component.form.nameContainsInvalidChars': 'Name contains invalid characters', + 'registration.page.registration.waitActivationWarning': 'Please wait for activation', + }) + .build(); + +describe('useRegisterErrorHandler', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should handle error-invalid-email error', () => { + const { result } = renderHook(() => useRegisterErrorHandler(mockSetError, mockSetServerError, mockSetLoginRoute), { + wrapper: defaultAppRoot, + }); + + const error = { error: 'error-invalid-email' }; + result.current.handleRegisterError(error); + + expect(mockSetError).toHaveBeenCalledWith('email', { + type: 'invalid-email', + message: 'Invalid email format', + }); + }); + + it('should handle error-user-already-exists error', () => { + const { result } = renderHook(() => useRegisterErrorHandler(mockSetError, mockSetServerError, mockSetLoginRoute), { + wrapper: defaultAppRoot, + }); + + const error = { errorType: 'error-user-already-exists' }; + result.current.handleRegisterError(error); + + expect(mockSetError).toHaveBeenCalledWith('username', { + type: 'user-already-exists', + message: 'Username already exists', + }); + }); + + it('should handle "Email already exists" error', () => { + const { result } = renderHook(() => useRegisterErrorHandler(mockSetError, mockSetServerError, mockSetLoginRoute), { + wrapper: defaultAppRoot, + }); + + const error = { error: 'Email already exists for user' }; + result.current.handleRegisterError(error); + + expect(mockSetError).toHaveBeenCalledWith('email', { + type: 'email-already-exists', + message: 'Email already exists', + }); + }); + + it('should handle "Username is already in use" error', () => { + const { result } = renderHook(() => useRegisterErrorHandler(mockSetError, mockSetServerError, mockSetLoginRoute), { + wrapper: defaultAppRoot, + }); + + const error = { error: 'Username is already in use' }; + result.current.handleRegisterError(error); + + expect(mockSetError).toHaveBeenCalledWith('username', { + type: 'username-already-exists', + message: 'User already exists', + }); + }); + + it('should handle "The username provided is not valid" error', () => { + const { result } = renderHook(() => useRegisterErrorHandler(mockSetError, mockSetServerError, mockSetLoginRoute), { + wrapper: defaultAppRoot, + }); + + const error = { error: 'The username provided is not valid' }; + result.current.handleRegisterError(error); + + expect(mockSetError).toHaveBeenCalledWith('username', { + type: 'username-contains-invalid-chars', + message: 'Username contains invalid characters', + }); + }); + + it('should handle "Name contains invalid characters" error', () => { + const { result } = renderHook(() => useRegisterErrorHandler(mockSetError, mockSetServerError, mockSetLoginRoute), { + wrapper: defaultAppRoot, + }); + + const error = { error: 'Name contains invalid characters' }; + result.current.handleRegisterError(error); + + expect(mockSetError).toHaveBeenCalledWith('name', { + type: 'name-contains-invalid-chars', + message: 'Name contains invalid characters', + }); + }); + + it('should handle error-too-many-requests error with toast', () => { + const { result } = renderHook(() => useRegisterErrorHandler(mockSetError, mockSetServerError, mockSetLoginRoute), { + wrapper: defaultAppRoot, + }); + + const error = { error: 'error-too-many-requests' }; + result.current.handleRegisterError(error); + + expect(mockDispatchToastMessage).toHaveBeenCalledWith({ + type: 'error', + message: 'error-too-many-requests', + }); + }); + + it('should handle error-user-is-not-activated error', () => { + const { result } = renderHook(() => useRegisterErrorHandler(mockSetError, mockSetServerError, mockSetLoginRoute), { + wrapper: defaultAppRoot, + }); + + const error = { error: 'error-user-is-not-activated' }; + result.current.handleRegisterError(error); + + expect(mockDispatchToastMessage).toHaveBeenCalledWith({ + type: 'info', + message: 'Please wait for activation', + }); + expect(mockSetLoginRoute).toHaveBeenCalledWith('login'); + }); + + it('should handle error-user-registration-custom-field error', () => { + const { result } = renderHook(() => useRegisterErrorHandler(mockSetError, mockSetServerError, mockSetLoginRoute), { + wrapper: defaultAppRoot, + }); + + const error = { + error: 'error-user-registration-custom-field', + message: 'Custom field validation failed', + }; + result.current.handleRegisterError(error); + + expect(mockSetServerError).toHaveBeenCalledWith('Custom field validation failed'); + }); +}); diff --git a/packages/web-ui-registration/src/hooks/useRegisterFormValidation.spec.tsx b/packages/web-ui-registration/src/hooks/useRegisterFormValidation.spec.tsx new file mode 100644 index 0000000000000..a16644a525268 --- /dev/null +++ b/packages/web-ui-registration/src/hooks/useRegisterFormValidation.spec.tsx @@ -0,0 +1,114 @@ +import { renderHook } from '@testing-library/react'; +import { mockAppRoot } from '@rocket.chat/mock-providers'; + +import { useRegisterFormValidation } from './useRegisterFormValidation'; + +const mockWatch = jest.fn(); + +const defaultAppRoot = mockAppRoot() + .withTranslations('en', 'core', { + Required_field: '{{field}} is required', + 'registration.component.form.name': 'Name', + 'registration.component.form.email': 'Email', + 'registration.component.form.username': 'Username', + 'registration.component.form.password': 'Password', + 'registration.component.form.confirmPassword': 'Confirm password', + 'registration.component.form.reasonToJoin': 'Reason to join', + 'registration.component.form.invalidEmail': 'Invalid email format', + 'registration.component.form.invalidConfirmPass': "Passwords don't match", + Password_must_meet_the_complexity_requirements: 'Password must meet complexity requirements', + }) + .withSetting('Accounts_RequireNameForSignUp', true) + .withSetting('Accounts_RequirePasswordConfirmation', true) + .withSetting('Accounts_ManuallyApproveNewUsers', false) + .withSetting('Accounts_Password_Policy_Enabled', false) + .build(); + +describe('useRegisterFormValidation', () => { + beforeEach(() => { + mockWatch.mockReturnValue({ password: '' }); + }); + + it('should return validation rules', () => { + const { result } = renderHook(() => useRegisterFormValidation(mockWatch as any), { + wrapper: defaultAppRoot, + }); + + expect(result.current.validationRules).toBeDefined(); + expect(result.current.validationRules.name).toBeDefined(); + expect(result.current.validationRules.email).toBeDefined(); + expect(result.current.validationRules.username).toBeDefined(); + expect(result.current.validationRules.password).toBeDefined(); + expect(result.current.validationRules.passwordConfirmation).toBeDefined(); + expect(result.current.validationRules.reason).toBeDefined(); + }); + + it('should return settings values', () => { + const { result } = renderHook(() => useRegisterFormValidation(mockWatch as any), { + wrapper: defaultAppRoot, + }); + + expect(result.current.requireNameForRegister).toBe(true); + expect(result.current.requiresPasswordConfirmation).toBe(true); + expect(result.current.manuallyApproveNewUsersRequired).toBe(false); + }); + + it('should set name as optional when not required', () => { + const appRoot = mockAppRoot() + .withTranslations('en', 'core', { + 'registration.component.form.name': 'Name', + }) + .withSetting('Accounts_RequireNameForSignUp', false) + .build(); + + const { result } = renderHook(() => useRegisterFormValidation(mockWatch as any), { + wrapper: appRoot, + }); + + expect(result.current.validationRules.name.required).toBe(false); + expect(result.current.requireNameForRegister).toBe(false); + }); + + it('should include email pattern validation', () => { + const { result } = renderHook(() => useRegisterFormValidation(mockWatch as any), { + wrapper: defaultAppRoot, + }); + + expect(result.current.validationRules.email.pattern).toBeDefined(); + expect(result.current.validationRules.email.pattern.value).toBeInstanceOf(RegExp); + }); + + it('should validate password confirmation matches password', () => { + mockWatch.mockImplementation((field?: string) => { + if (field === 'password') return 'test123'; + return { password: 'test123' }; + }); + + const { result } = renderHook(() => useRegisterFormValidation(mockWatch as any), { + wrapper: defaultAppRoot, + }); + + const validateFn = result.current.validationRules.passwordConfirmation.validate; + expect(validateFn('test123')).toBe(true); + expect(validateFn('different')).toBe("Passwords don't match"); + }); + + it('should return passwordIsValid based on password policy', () => { + mockWatch.mockReturnValue({ password: 'ValidPassword123!' }); + + const { result } = renderHook(() => useRegisterFormValidation(mockWatch as any), { + wrapper: defaultAppRoot, + }); + + // With disabled password policy, any password is valid + expect(result.current.passwordIsValid).toBe(true); + }); + + it('should have correct password dependencies', () => { + const { result } = renderHook(() => useRegisterFormValidation(mockWatch as any), { + wrapper: defaultAppRoot, + }); + + expect(result.current.validationRules.passwordConfirmation.deps).toEqual(['password']); + }); +}); From 44170215697b6295ab085de185b49d997e2ade15 Mon Sep 17 00:00:00 2001 From: Chetan Agarwal Date: Wed, 28 Jan 2026 01:48:41 +0530 Subject: [PATCH 3/4] fix: Per-test userEvent instance and CustomFieldsForm test --- .../src/RegisterForm.spec.tsx | 75 +++++++++++-------- 1 file changed, 43 insertions(+), 32 deletions(-) diff --git a/packages/web-ui-registration/src/RegisterForm.spec.tsx b/packages/web-ui-registration/src/RegisterForm.spec.tsx index 3c53de3c0709b..c9c700d9a6c62 100644 --- a/packages/web-ui-registration/src/RegisterForm.spec.tsx +++ b/packages/web-ui-registration/src/RegisterForm.spec.tsx @@ -7,6 +7,9 @@ import { RegisterForm } from './RegisterForm'; const mockSetLoginRoute = jest.fn(); const mockRegisterMethod = jest.fn(); +// Per-test user instance +let user: ReturnType; + // Mock the useRegisterMethod hook jest.mock('./hooks/useRegisterMethod', () => ({ useRegisterMethod: () => ({ @@ -15,7 +18,7 @@ jest.mock('./hooks/useRegisterMethod', () => ({ }), })); -const defaultAppRoot = mockAppRoot() +const defaultApp Root = mockAppRoot() .withTranslations('en', 'core', { Required_field: '{{field}} is required', 'registration.component.form.createAnAccount': 'Create an account', @@ -47,6 +50,7 @@ const defaultAppRoot = mockAppRoot() describe('RegisterForm', () => { beforeEach(() => { jest.clearAllMocks(); + user = userEvent.setup(); }); describe('Rendering', () => { @@ -154,8 +158,8 @@ describe('RegisterForm', () => { render(, { wrapper: appRoot }); const nameInput = screen.getByRole('textbox', { name: /name/i }); - await userEvent.click(nameInput); - await userEvent.tab(); + await user.click(nameInput); + await user.tab(); await waitFor(() => { expect(nameInput).toHaveAccessibleDescription(/name is required/i); @@ -173,8 +177,8 @@ describe('RegisterForm', () => { render(, { wrapper: appRoot }); const emailInput = screen.getByRole('textbox', { name: /email/i }); - await userEvent.click(emailInput); - await userEvent.tab(); + await user.click(emailInput); + await user.tab(); await waitFor(() => { expect(emailInput).toHaveAccessibleDescription(/email is required/i); @@ -191,8 +195,8 @@ describe('RegisterForm', () => { render(, { wrapper: appRoot }); const emailInput = screen.getByRole('textbox', { name: /email/i }); - await userEvent.type(emailInput, 'invalid-email'); - await userEvent.tab(); + await user.type(emailInput, 'invalid-email'); + await user.tab(); await waitFor(() => { expect(emailInput).toHaveAccessibleDescription(/invalid email format/i); @@ -203,8 +207,8 @@ describe('RegisterForm', () => { render(, { wrapper: defaultAppRoot }); const emailInput = screen.getByRole('textbox', { name: /email/i }); - await userEvent.type(emailInput, 'test@example.com'); - await userEvent.tab(); + await user.type(emailInput, 'test@example.com'); + await user.tab(); await waitFor(() => { expect(emailInput).not.toHaveAccessibleDescription(); @@ -222,8 +226,8 @@ describe('RegisterForm', () => { render(, { wrapper: appRoot }); const usernameInput = screen.getByRole('textbox', { name: /username/i }); - await userEvent.click(usernameInput); - await userEvent.tab(); + await user.click(usernameInput); + await user.tab(); await waitFor(() => { expect(usernameInput).toHaveAccessibleDescription(/username is required/i); @@ -246,9 +250,9 @@ describe('RegisterForm', () => { const passwordInput = screen.getByLabelText(/^password/i); const confirmPasswordInput = screen.getByLabelText(/confirm password/i); - await userEvent.type(passwordInput, 'password123'); - await userEvent.type(confirmPasswordInput, 'password456'); - await userEvent.tab(); + await user.type(passwordInput, 'password123'); + await user.type(confirmPasswordInput, 'password456'); + await user.tab(); await waitFor(() => { expect(confirmPasswordInput).toHaveAccessibleDescription(/passwords don't match/i); @@ -274,13 +278,13 @@ describe('RegisterForm', () => { render(, { wrapper: appRoot }); // Fill in the form - await userEvent.type(screen.getByRole('textbox', { name: /name/i }), 'John Doe'); - await userEvent.type(screen.getByRole('textbox', { name: /email/i }), 'john@example.com'); - await userEvent.type(screen.getByRole('textbox', { name: /username/i }), 'johndoe'); - await userEvent.type(screen.getByLabelText(/password/i), 'password123'); + await user.type(screen.getByRole('textbox', { name: /name/i }), 'John Doe'); + await user.type(screen.getByRole('textbox', { name: /email/i }), 'john@example.com'); + await user.type(screen.getByRole('textbox', { name: /username/i }), 'johndoe'); + await user.type(screen.getByLabelText(/password/i), 'password123'); // Submit the form - await userEvent.click(screen.getByRole('button', { name: /join your team/i })); + await user.click(screen.getByRole('button', { name: /join your team/i })); await waitFor(() => { expect(mockRegisterMethod).toHaveBeenCalledWith( @@ -307,13 +311,13 @@ describe('RegisterForm', () => { render(, { wrapper: appRoot }); - await userEvent.type(screen.getByRole('textbox', { name: /name/i }), 'John Doe'); - await userEvent.type(screen.getByRole('textbox', { name: /email/i }), 'john@example.com'); - await userEvent.type(screen.getByRole('textbox', { name: /username/i }), 'johndoe'); - await userEvent.type(screen.getByLabelText(/^password/i), 'password123'); - await userEvent.type(screen.getByLabelText(/confirm password/i), 'password123'); + await user.type(screen.getByRole('textbox', { name: /name/i }), 'John Doe'); + await user.type(screen.getByRole('textbox', { name: /email/i }), 'john@example.com'); + await user.type(screen.getByRole('textbox', { name: /username/i }), 'johndoe'); + await user.type(screen.getByLabelText(/^password/i), 'password123'); + await user.type(screen.getByLabelText(/confirm password/i), 'password123'); - await userEvent.click(screen.getByRole('button', { name: /join your team/i })); + await user.click(screen.getByRole('button', { name: /join your team/i })); await waitFor(() => { expect(mockRegisterMethod).toHaveBeenCalled(); @@ -347,20 +351,27 @@ describe('RegisterForm', () => { render(, { wrapper: defaultAppRoot }); const backLink = screen.getByText(/back to login/i); - await userEvent.click(backLink); + await user.click(backLink); expect(mockSetLoginRoute).toHaveBeenCalledWith('login'); }); }); describe('Custom Fields Integration', () => { - it('should render custom fields form', () => { - // The CustomFieldsForm is already tested in ui-client - // We just verify it's rendered in RegisterForm - render(, { wrapper: defaultAppRoot }); + it('should render custom fields when available', () => { + // Mock custom fields to verify CustomFieldsForm integration + const mockCustomFields = [ + { _id: 'field1', type: 'text', label: 'Custom Field 1', required: true, defaultValue: '', public: true }, + ]; + + const appRoot = mockAppRoot() + .withTranslations('en', 'core', {}) + .withEndpoint('GET', '/v1/custom-fields.public', () => ({ customFields: mockCustomFields })) + .build(); + + render(, { wrapper: appRoot }); - // CustomFieldsForm component should be in the document - // This is an integration test to ensure the component is properly included + // Verify CustomFieldsForm integration - detailed tests are in ui-client const form = screen.getByRole('form'); expect(form).toBeInTheDocument(); }); From a870b2a6675d85a6a784d21d46282e92c3e8a549 Mon Sep 17 00:00:00 2001 From: Chetan Agarwal Date: Wed, 28 Jan 2026 02:01:08 +0530 Subject: [PATCH 4/4] Solved defaultAppRoot issue --- packages/web-ui-registration/src/RegisterForm.spec.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web-ui-registration/src/RegisterForm.spec.tsx b/packages/web-ui-registration/src/RegisterForm.spec.tsx index c9c700d9a6c62..02725abc0f8fb 100644 --- a/packages/web-ui-registration/src/RegisterForm.spec.tsx +++ b/packages/web-ui-registration/src/RegisterForm.spec.tsx @@ -18,7 +18,7 @@ jest.mock('./hooks/useRegisterMethod', () => ({ }), })); -const defaultApp Root = mockAppRoot() +const defaultAppRoot = mockAppRoot() .withTranslations('en', 'core', { Required_field: '{{field}} is required', 'registration.component.form.createAnAccount': 'Create an account',