import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { AddBeneficiaryModal } from '../AddBeneficiaryModal';
import * as api from '../../lib/api';
jest.mock('../../lib/api');
const mockCreateBeneficiary = api.createBeneficiary as jest.MockedFunction<
typeof api.createBeneficiary
>;
describe('AddBeneficiaryModal', () => {
const defaultProps = {
isOpen: true,
onClose: jest.fn(),
onSuccess: jest.fn(),
};
beforeEach(() => {
jest.clearAllMocks();
});
const getNameInput = () => screen.getByPlaceholderText('e.g., Grandma Julia');
const getAddressInput = () => screen.getByPlaceholderText('123 Main St, City, State');
const getPhoneInput = () => screen.getByPlaceholderText('+1 (555) 123-4567');
const getSubmitButton = () => screen.getByRole('button', { name: /Add Beneficiary/i });
const getCancelButton = () => screen.getByRole('button', { name: /Cancel/i });
it('does not render when isOpen is false', () => {
render();
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
});
it('renders modal when isOpen is true', () => {
render();
expect(screen.getByRole('dialog')).toBeInTheDocument();
expect(screen.getByText('Add New Beneficiary')).toBeInTheDocument();
});
it('renders all form fields', () => {
render();
expect(getNameInput()).toBeInTheDocument();
expect(getAddressInput()).toBeInTheDocument();
expect(getPhoneInput()).toBeInTheDocument();
});
it('shows validation error when name is empty', async () => {
render();
fireEvent.click(getSubmitButton());
await waitFor(() => {
expect(screen.getByText('Please enter a name for the beneficiary')).toBeInTheDocument();
});
expect(mockCreateBeneficiary).not.toHaveBeenCalled();
expect(defaultProps.onSuccess).not.toHaveBeenCalled();
});
it('calls createBeneficiary with correct data on submit', async () => {
mockCreateBeneficiary.mockResolvedValueOnce({ id: 123, name: 'Test Name' });
render();
fireEvent.change(getNameInput(), { target: { value: 'Grandma Julia' } });
fireEvent.change(getAddressInput(), { target: { value: '123 Main St' } });
fireEvent.change(getPhoneInput(), { target: { value: '+1 555-123-4567' } });
fireEvent.click(getSubmitButton());
await waitFor(() => {
expect(mockCreateBeneficiary).toHaveBeenCalledWith({
name: 'Grandma Julia',
address: '123 Main St',
phone: '+1 555-123-4567',
});
});
});
it('calls onSuccess and onClose after successful creation', async () => {
mockCreateBeneficiary.mockResolvedValueOnce({ id: 456, name: 'Test Person' });
render();
await userEvent.type(getNameInput(), 'Test Person');
fireEvent.click(getSubmitButton());
await waitFor(() => {
expect(defaultProps.onSuccess).toHaveBeenCalledWith({
id: 456,
name: 'Test Person',
});
expect(defaultProps.onClose).toHaveBeenCalled();
});
});
it('shows error message on API failure', async () => {
mockCreateBeneficiary.mockRejectedValueOnce(new Error('Network error'));
render();
await userEvent.type(getNameInput(), 'Test Name');
fireEvent.click(getSubmitButton());
await waitFor(() => {
expect(screen.getByText('Network error')).toBeInTheDocument();
});
expect(defaultProps.onClose).not.toHaveBeenCalled();
expect(defaultProps.onSuccess).not.toHaveBeenCalled();
});
it('closes modal when Cancel button is clicked', () => {
render();
fireEvent.click(getCancelButton());
expect(defaultProps.onClose).toHaveBeenCalled();
});
it('closes modal when clicking backdrop', () => {
render();
const backdrop = screen.getByRole('dialog');
fireEvent.click(backdrop);
expect(defaultProps.onClose).toHaveBeenCalled();
});
it('does not close modal when clicking inside the modal content', () => {
render();
const modalTitle = screen.getByText('Add New Beneficiary');
fireEvent.click(modalTitle);
expect(defaultProps.onClose).not.toHaveBeenCalled();
});
it('closes modal when Escape key is pressed', () => {
render();
fireEvent.keyDown(document, { key: 'Escape' });
expect(defaultProps.onClose).toHaveBeenCalled();
});
it('disables form during submission', async () => {
let resolvePromise: (value: { id: number; name: string }) => void;
const promise = new Promise<{ id: number; name: string }>((resolve) => {
resolvePromise = resolve;
});
mockCreateBeneficiary.mockReturnValueOnce(promise);
render();
await userEvent.type(getNameInput(), 'Test Name');
fireEvent.click(getSubmitButton());
await waitFor(() => {
expect(getNameInput()).toBeDisabled();
expect(getCancelButton()).toBeDisabled();
});
resolvePromise!({ id: 1, name: 'Test Name' });
});
it('clears form when modal is reopened', async () => {
const { rerender } = render();
await userEvent.type(getNameInput(), 'Some Name');
rerender();
rerender();
expect(getNameInput()).toHaveValue('');
});
it('trims whitespace from inputs', async () => {
mockCreateBeneficiary.mockResolvedValueOnce({ id: 789, name: 'Trimmed Name' });
render();
await userEvent.type(getNameInput(), ' Trimmed Name ');
fireEvent.click(getSubmitButton());
await waitFor(() => {
expect(mockCreateBeneficiary).toHaveBeenCalledWith({
name: 'Trimmed Name',
address: undefined,
phone: undefined,
});
});
});
it('dismisses error when user starts typing', async () => {
render();
fireEvent.click(getSubmitButton());
await waitFor(() => {
expect(screen.getByText('Please enter a name for the beneficiary')).toBeInTheDocument();
});
await userEvent.type(getNameInput(), 'N');
expect(screen.queryByText('Please enter a name for the beneficiary')).not.toBeInTheDocument();
});
});