WellNuo/admin/components/__tests__/AddBeneficiaryModal.test.tsx
Sergei d530695b8b Add Add Beneficiary Flow for web admin
- Add AddBeneficiaryModal component with form fields for name, address, phone
- Add createBeneficiary and updateBeneficiary API methods in admin/lib/api.js
- Update Beneficiaries page with Add button and modal integration
- Add comprehensive tests for AddBeneficiaryModal (15 test cases)

Features:
- Modal with form validation (name required)
- Keyboard support (Escape to close, Enter to submit)
- Click outside to dismiss
- Loading state during submission
- Error handling and display
- Form resets on modal reopen
- Automatic redirect to beneficiary detail after creation
2026-02-01 08:32:59 -08:00

209 lines
6.6 KiB
TypeScript

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(<AddBeneficiaryModal {...defaultProps} isOpen={false} />);
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
});
it('renders modal when isOpen is true', () => {
render(<AddBeneficiaryModal {...defaultProps} />);
expect(screen.getByRole('dialog')).toBeInTheDocument();
expect(screen.getByText('Add New Beneficiary')).toBeInTheDocument();
});
it('renders all form fields', () => {
render(<AddBeneficiaryModal {...defaultProps} />);
expect(getNameInput()).toBeInTheDocument();
expect(getAddressInput()).toBeInTheDocument();
expect(getPhoneInput()).toBeInTheDocument();
});
it('shows validation error when name is empty', async () => {
render(<AddBeneficiaryModal {...defaultProps} />);
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(<AddBeneficiaryModal {...defaultProps} />);
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(<AddBeneficiaryModal {...defaultProps} />);
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(<AddBeneficiaryModal {...defaultProps} />);
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(<AddBeneficiaryModal {...defaultProps} />);
fireEvent.click(getCancelButton());
expect(defaultProps.onClose).toHaveBeenCalled();
});
it('closes modal when clicking backdrop', () => {
render(<AddBeneficiaryModal {...defaultProps} />);
const backdrop = screen.getByRole('dialog');
fireEvent.click(backdrop);
expect(defaultProps.onClose).toHaveBeenCalled();
});
it('does not close modal when clicking inside the modal content', () => {
render(<AddBeneficiaryModal {...defaultProps} />);
const modalTitle = screen.getByText('Add New Beneficiary');
fireEvent.click(modalTitle);
expect(defaultProps.onClose).not.toHaveBeenCalled();
});
it('closes modal when Escape key is pressed', () => {
render(<AddBeneficiaryModal {...defaultProps} />);
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(<AddBeneficiaryModal {...defaultProps} />);
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(<AddBeneficiaryModal {...defaultProps} />);
await userEvent.type(getNameInput(), 'Some Name');
rerender(<AddBeneficiaryModal {...defaultProps} isOpen={false} />);
rerender(<AddBeneficiaryModal {...defaultProps} isOpen={true} />);
expect(getNameInput()).toHaveValue('');
});
it('trims whitespace from inputs', async () => {
mockCreateBeneficiary.mockResolvedValueOnce({ id: 789, name: 'Trimmed Name' });
render(<AddBeneficiaryModal {...defaultProps} />);
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(<AddBeneficiaryModal {...defaultProps} />);
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();
});
});