import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import EmailComposer from '../EmailComposer'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import type { StaffEmail } from '../../../types'; // Mock react-i18next vi.mock('react-i18next', () => ({ useTranslation: () => ({ t: (key: string) => key, }), })); // Mock react-hot-toast vi.mock('react-hot-toast', () => ({ default: { success: vi.fn(), error: vi.fn(), }, })); // Mock hooks vi.mock('../../../hooks/useStaffEmail', () => ({ useCreateDraft: vi.fn(() => ({ mutateAsync: vi.fn(), isPending: false, })), useUpdateDraft: vi.fn(() => ({ mutateAsync: vi.fn(), isPending: false, })), useSendEmail: vi.fn(() => ({ mutateAsync: vi.fn(), isPending: false, })), useUploadAttachment: vi.fn(() => ({ mutateAsync: vi.fn(), })), useContactSearch: vi.fn(() => ({ data: [], })), useUserEmailAddresses: vi.fn(() => ({ data: [ { id: 1, email_address: 'test@example.com', display_name: 'Test User', }, ], })), })); describe('EmailComposer', () => { const defaultProps = { onClose: vi.fn(), onSent: vi.fn(), }; const mockReplyToEmail: StaffEmail = { id: 1, owner: 1, emailAddress: 1, folder: 1, fromAddress: 'sender@example.com', fromName: 'Sender Name', toAddresses: ['test@example.com'], ccAddresses: [], bccAddresses: [], subject: 'Original Subject', snippet: 'Email snippet', bodyText: 'Original email body', bodyHtml: '

Original email body

', messageId: 'message-123', inReplyTo: null, references: '', status: 'SENT', threadId: 'thread-123', emailDate: '2025-01-15T10:00:00Z', isRead: true, isStarred: false, isImportant: false, isAnswered: false, isPermanentlyDeleted: false, deletedAt: null, hasAttachments: false, attachmentCount: 0, attachments: [], labels: [], createdAt: '2025-01-15T10:00:00Z', updatedAt: '2025-01-15T10:00:00Z', }; const createWrapper = () => { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false }, }, }); return ({ children }: { children: React.ReactNode }) => ( {children} ); }; beforeEach(() => { vi.clearAllMocks(); }); it('renders new message mode', () => { render(, { wrapper: createWrapper() }); expect(screen.getByText('New Message')).toBeInTheDocument(); }); it('renders reply mode with subject prefixed', () => { render(, { wrapper: createWrapper(), }); expect(screen.getByText('Reply')).toBeInTheDocument(); expect(screen.getByDisplayValue('Re: Original Subject')).toBeInTheDocument(); }); it('renders forward mode with subject prefixed', () => { render(, { wrapper: createWrapper(), }); expect(screen.getByText('Forward')).toBeInTheDocument(); expect(screen.getByDisplayValue('Fwd: Original Subject')).toBeInTheDocument(); }); it('populates recipient in reply mode', () => { render(, { wrapper: createWrapper(), }); expect(screen.getByDisplayValue('sender@example.com')).toBeInTheDocument(); }); it('renders minimized state', () => { render(, { wrapper: createWrapper() }); const buttons = screen.getAllByRole('button'); const minimizeButton = buttons.find((btn) => btn.querySelector('svg')?.classList.contains('lucide-minimize-2') ); if (minimizeButton) { fireEvent.click(minimizeButton); } expect(screen.getByText('New Message')).toBeInTheDocument(); }); it('expands from minimized state', () => { render(, { wrapper: createWrapper() }); // Minimize let buttons = screen.getAllByRole('button'); const minimizeButton = buttons.find((btn) => btn.querySelector('svg')?.classList.contains('lucide-minimize-2') ); if (minimizeButton) { fireEvent.click(minimizeButton); } // Maximize buttons = screen.getAllByRole('button'); const maximizeButton = buttons.find((btn) => btn.querySelector('svg')?.classList.contains('lucide-maximize-2') ); if (maximizeButton) { fireEvent.click(maximizeButton); } expect(screen.getByPlaceholderText('recipient@example.com')).toBeInTheDocument(); }); it('calls onClose when close button clicked', () => { render(, { wrapper: createWrapper() }); const buttons = screen.getAllByRole('button'); const closeButton = buttons.find((btn) => btn.querySelector('svg')?.classList.contains('lucide-x') ); if (closeButton) { fireEvent.click(closeButton); } expect(defaultProps.onClose).toHaveBeenCalled(); }); it('shows Cc field when Cc button clicked', () => { render(, { wrapper: createWrapper() }); const ccButton = screen.getByText('Cc'); fireEvent.click(ccButton); expect(screen.getByPlaceholderText('cc@example.com')).toBeInTheDocument(); }); it('shows Bcc field when Bcc button clicked', () => { render(, { wrapper: createWrapper() }); const bccButton = screen.getByText('Bcc'); fireEvent.click(bccButton); expect(screen.getByPlaceholderText('bcc@example.com')).toBeInTheDocument(); }); it('updates subject field', () => { render(, { wrapper: createWrapper() }); const subjectInput = screen.getByPlaceholderText('Email subject'); fireEvent.change(subjectInput, { target: { value: 'Test Subject' } }); expect(subjectInput).toHaveValue('Test Subject'); }); it('updates body field', () => { render(, { wrapper: createWrapper() }); const bodyTextarea = screen.getByPlaceholderText('Write your message...'); fireEvent.change(bodyTextarea, { target: { value: 'Test email body' } }); expect(bodyTextarea).toHaveValue('Test email body'); }); it('updates to field', () => { render(, { wrapper: createWrapper() }); const toInput = screen.getByPlaceholderText('recipient@example.com'); fireEvent.change(toInput, { target: { value: 'test@example.com' } }); expect(toInput).toHaveValue('test@example.com'); }); it('updates cc field when shown', () => { render(, { wrapper: createWrapper() }); fireEvent.click(screen.getByText('Cc')); const ccInput = screen.getByPlaceholderText('cc@example.com'); fireEvent.change(ccInput, { target: { value: 'cc@example.com' } }); expect(ccInput).toHaveValue('cc@example.com'); }); it('updates bcc field when shown', () => { render(, { wrapper: createWrapper() }); fireEvent.click(screen.getByText('Bcc')); const bccInput = screen.getByPlaceholderText('bcc@example.com'); fireEvent.change(bccInput, { target: { value: 'bcc@example.com' } }); expect(bccInput).toHaveValue('bcc@example.com'); }); it('renders send button', () => { render(, { wrapper: createWrapper() }); expect(screen.getByText('staffEmail.send')).toBeInTheDocument(); }); it('renders save draft button', () => { render(, { wrapper: createWrapper() }); expect(screen.getByText('Save draft')).toBeInTheDocument(); }); it('renders formatting buttons', () => { render(, { wrapper: createWrapper() }); const buttons = screen.getAllByRole('button'); const boldButton = buttons.find((btn) => btn.querySelector('svg')?.classList.contains('lucide-bold') ); const italicButton = buttons.find((btn) => btn.querySelector('svg')?.classList.contains('lucide-italic') ); const underlineButton = buttons.find((btn) => btn.querySelector('svg')?.classList.contains('lucide-underline') ); expect(boldButton).toBeInTheDocument(); expect(italicButton).toBeInTheDocument(); expect(underlineButton).toBeInTheDocument(); }); it('renders attachment button', () => { render(, { wrapper: createWrapper() }); // Attachment input is wrapped in a label, not a button const paperclipIcons = document.querySelectorAll('.lucide-paperclip'); expect(paperclipIcons.length).toBeGreaterThan(0); }); it('renders discard button', () => { render(, { wrapper: createWrapper() }); const buttons = screen.getAllByRole('button'); const discardButton = buttons.find((btn) => btn.querySelector('svg')?.classList.contains('lucide-trash-2') ); expect(discardButton).toBeInTheDocument(); }); it('renders from address selector', () => { render(, { wrapper: createWrapper() }); expect(screen.getByText('From:')).toBeInTheDocument(); expect(screen.getByRole('combobox')).toBeInTheDocument(); }); it('populates from address with default value', async () => { render(, { wrapper: createWrapper() }); await waitFor(() => { const select = screen.getByRole('combobox'); expect(select).toHaveValue('1'); }); }); it('does not add Re: prefix if subject already has it', () => { const emailWithReply = { ...mockReplyToEmail, subject: 'Re: Already Replied', }; render(, { wrapper: createWrapper(), }); expect(screen.getByDisplayValue('Re: Already Replied')).toBeInTheDocument(); }); it('does not add Fwd: prefix if subject already has it', () => { const emailWithFwd = { ...mockReplyToEmail, subject: 'Fwd: Already Forwarded', }; render(, { wrapper: createWrapper(), }); expect(screen.getByDisplayValue('Fwd: Already Forwarded')).toBeInTheDocument(); }); it('includes original message in reply body', () => { render(, { wrapper: createWrapper(), }); const bodyTextarea = screen.getByPlaceholderText('Write your message...'); const value = (bodyTextarea as HTMLTextAreaElement).value; expect(value).toContain('Original email body'); }); it('includes original message in forward body', () => { render(, { wrapper: createWrapper(), }); const bodyTextarea = screen.getByPlaceholderText('Write your message...'); const value = (bodyTextarea as HTMLTextAreaElement).value; expect(value).toContain('Original email body'); }); it('renders with all header fields', () => { render(, { wrapper: createWrapper() }); expect(screen.getByText('From:')).toBeInTheDocument(); expect(screen.getByText('To:')).toBeInTheDocument(); expect(screen.getByText('Subject:')).toBeInTheDocument(); }); });