import { render, screen, fireEvent, waitFor, act } from '@testing-library/react' import { ProfileForm } from '../profile-form' import { ChangePasswordForm } from '../change-password-form' // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- function changeInput(element: HTMLElement, value: string) { fireEvent.change(element, { target: { value } }) } // --------------------------------------------------------------------------- // ProfileForm // --------------------------------------------------------------------------- const defaultProfileProps = { initialValues: { firstName: 'Jane', lastName: 'Doe', email: 'jane@example.com' }, onSubmit: jest.fn(), } describe('ProfileForm', () => { beforeEach(() => { jest.clearAllMocks() }) it('renders all fields with initial values', () => { render() expect(screen.getByLabelText('Email')).toHaveValue('jane@example.com') expect(screen.getByLabelText('First name')).toHaveValue('Jane') expect(screen.getByLabelText('Last name')).toHaveValue('Doe') }) it('email field is always disabled', () => { render() expect(screen.getByLabelText('Email')).toBeDisabled() }) it('save button is disabled when no changes have been made', () => { render() expect(screen.getByRole('button', { name: /save changes/i })).toBeDisabled() }) it('save button becomes enabled after editing first name', () => { render() changeInput(screen.getByLabelText('First name'), 'John') expect(screen.getByRole('button', { name: /save changes/i })).toBeEnabled() }) it('save button becomes enabled after editing last name', () => { render() changeInput(screen.getByLabelText('Last name'), 'Smith') expect(screen.getByRole('button', { name: /save changes/i })).toBeEnabled() }) it('save button is disabled again when edits are reverted', () => { render() changeInput(screen.getByLabelText('First name'), 'John') expect(screen.getByRole('button', { name: /save changes/i })).toBeEnabled() changeInput(screen.getByLabelText('First name'), 'Jane') expect(screen.getByRole('button', { name: /save changes/i })).toBeDisabled() }) it('calls onSubmit with trimmed first_name and last_name', async () => { const onSubmit = jest.fn().mockResolvedValue(undefined) render() changeInput(screen.getByLabelText('First name'), ' John ') fireEvent.submit(screen.getByRole('button', { name: /save changes/i }).closest('form')!) await waitFor(() => { expect(onSubmit).toHaveBeenCalledWith({ first_name: 'John', last_name: 'Doe', }) }) }) it('shows success message after successful submit', async () => { const onSubmit = jest.fn().mockResolvedValue(undefined) render() changeInput(screen.getByLabelText('First name'), 'John') fireEvent.submit(screen.getByRole('button', { name: /save changes/i }).closest('form')!) await waitFor(() => { expect(screen.getByText('Profile updated.')).toBeInTheDocument() }) }) it('shows error message when onSubmit throws', async () => { const onSubmit = jest.fn().mockRejectedValue(new Error('Server error')) render() changeInput(screen.getByLabelText('First name'), 'John') fireEvent.submit(screen.getByRole('button', { name: /save changes/i }).closest('form')!) await waitFor(() => { expect(screen.getByText('Server error')).toBeInTheDocument() }) }) it('shows generic error message when thrown value is not an Error', async () => { const onSubmit = jest.fn().mockRejectedValue('oops') render() changeInput(screen.getByLabelText('First name'), 'John') fireEvent.submit(screen.getByRole('button', { name: /save changes/i }).closest('form')!) await waitFor(() => { expect(screen.getByText('Failed to update profile')).toBeInTheDocument() }) }) it('disables inputs when disabled prop is true', () => { render() expect(screen.getByLabelText('First name')).toBeDisabled() expect(screen.getByLabelText('Last name')).toBeDisabled() }) it('renders card title', () => { render() expect(screen.getByText('Profile')).toBeInTheDocument() }) it('renders card description', () => { render() expect(screen.getByText('Update your personal information.')).toBeInTheDocument() }) it('renders email-cannot-be-changed hint', () => { render() expect(screen.getByText('Email cannot be changed.')).toBeInTheDocument() }) }) // --------------------------------------------------------------------------- // ChangePasswordForm // --------------------------------------------------------------------------- const defaultPasswordProps = { onSubmit: jest.fn(), } describe('ChangePasswordForm', () => { beforeEach(() => { jest.clearAllMocks() }) it('renders both password fields (current + new, no confirm — startsim-nbq)', () => { render() expect(screen.getByLabelText('Current password')).toBeInTheDocument() expect(screen.getByLabelText('New password')).toBeInTheDocument() expect(screen.queryByLabelText('Confirm new password')).not.toBeInTheDocument() }) it('submit button is disabled when fields are empty', () => { render() expect(screen.getByRole('button', { name: /change password/i })).toBeDisabled() }) it('submit button is disabled when new password is less than 8 characters', () => { render() changeInput(screen.getByLabelText('Current password'), 'oldpassword') changeInput(screen.getByLabelText('New password'), 'short') expect(screen.getByRole('button', { name: /change password/i })).toBeDisabled() }) it('shows password length hint when new password has 1-7 chars', () => { render() changeInput(screen.getByLabelText('New password'), 'abc') expect(screen.getByText('Must be at least 8 characters.')).toBeInTheDocument() }) it('hides length hint when new password is empty', () => { render() // Don't type anything — hint should not appear expect(screen.queryByText('Must be at least 8 characters.')).not.toBeInTheDocument() }) it('submit button is enabled when current + new (>=8) are filled', () => { render() changeInput(screen.getByLabelText('Current password'), 'oldpassword') changeInput(screen.getByLabelText('New password'), 'newpassword1') expect(screen.getByRole('button', { name: /change password/i })).toBeEnabled() }) it('calls onSubmit with { old_password, new_password } — no confirm', async () => { const onSubmit = jest.fn().mockResolvedValue(undefined) render() changeInput(screen.getByLabelText('Current password'), 'oldpassword') changeInput(screen.getByLabelText('New password'), 'newpassword1') fireEvent.click(screen.getByRole('button', { name: /change password/i })) await waitFor(() => { expect(onSubmit).toHaveBeenCalledWith({ old_password: 'oldpassword', new_password: 'newpassword1', }) }) }) it('shows success message and clears fields after successful submit', async () => { const onSubmit = jest.fn().mockResolvedValue(undefined) render() changeInput(screen.getByLabelText('Current password'), 'oldpassword') changeInput(screen.getByLabelText('New password'), 'newpassword1') fireEvent.click(screen.getByRole('button', { name: /change password/i })) await waitFor(() => { expect(screen.getByText('Password changed successfully.')).toBeInTheDocument() }) expect(screen.getByLabelText('Current password')).toHaveValue('') expect(screen.getByLabelText('New password')).toHaveValue('') }) it('calls onSuccess callback after successful submit', async () => { const onSubmit = jest.fn().mockResolvedValue(undefined) const onSuccess = jest.fn() render() changeInput(screen.getByLabelText('Current password'), 'oldpassword') changeInput(screen.getByLabelText('New password'), 'newpassword1') fireEvent.click(screen.getByRole('button', { name: /change password/i })) await waitFor(() => { expect(onSuccess).toHaveBeenCalledTimes(1) }) }) it('shows error message when onSubmit throws', async () => { const onSubmit = jest.fn().mockRejectedValue(new Error('Wrong password')) render() changeInput(screen.getByLabelText('Current password'), 'wrongold') changeInput(screen.getByLabelText('New password'), 'newpassword1') fireEvent.click(screen.getByRole('button', { name: /change password/i })) await waitFor(() => { expect(screen.getByText('Wrong password')).toBeInTheDocument() }) }) it('shows generic error when thrown value is not Error instance', async () => { const onSubmit = jest.fn().mockRejectedValue('oops') render() changeInput(screen.getByLabelText('Current password'), 'wrongold') changeInput(screen.getByLabelText('New password'), 'newpassword1') fireEvent.click(screen.getByRole('button', { name: /change password/i })) await waitFor(() => { expect(screen.getByText('Failed to change password')).toBeInTheDocument() }) }) it('disables both inputs when disabled prop is true', () => { render() expect(screen.getByLabelText('Current password')).toBeDisabled() expect(screen.getByLabelText('New password')).toBeDisabled() }) it('renders card heading', () => { render() // Use role query to specifically find the heading, not the button expect(screen.getByRole('heading', { name: /change password/i })).toBeInTheDocument() }) it('renders card description', () => { render() expect( screen.getByText(/update your password/i) ).toBeInTheDocument() }) })