- 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
209 lines
6.6 KiB
TypeScript
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();
|
|
});
|
|
});
|