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()
})
})