import { describe, it, expect, vi, beforeEach } from 'vitest'; import { renderHook, waitFor, act } from '@testing-library/react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import React from 'react'; import { staffEmailKeys, useStaffEmailFolders, useCreateStaffEmailFolder, useUpdateStaffEmailFolder, useDeleteStaffEmailFolder, useStaffEmail, useStaffEmailThread, useStaffEmailLabels, useCreateLabel, useUpdateLabel, useDeleteLabel, useAddLabelToEmail, useRemoveLabelFromEmail, useCreateDraft, useUpdateDraft, useDeleteDraft, useSendEmail, useReplyToEmail, useForwardEmail, useMarkAsRead, useMarkAsUnread, useStarEmail, useUnstarEmail, useArchiveEmail, useTrashEmail, useRestoreEmail, usePermanentlyDeleteEmail, useMoveEmails, useBulkEmailAction, useContactSearch, useUploadAttachment, useDeleteAttachment, useSyncEmails, useFullSyncEmails, useUserEmailAddresses, } from '../useStaffEmail'; import * as staffEmailApi from '../../api/staffEmail'; vi.mock('../../api/staffEmail'); const mockFolder = { id: 1, owner: 1, name: 'Inbox', folderType: 'inbox', emailCount: 10, unreadCount: 3, createdAt: '2024-01-01T00:00:00Z', updatedAt: '2024-01-01T00:00:00Z', }; const mockEmail = { id: 1, folder: 1, fromAddress: 'sender@example.com', fromName: 'Sender Name', toAddresses: [{ email: 'recipient@example.com', name: 'Recipient' }], subject: 'Test Email', snippet: 'This is a test...', status: 'received', isRead: false, isStarred: false, isImportant: false, hasAttachments: false, attachmentCount: 0, threadId: 'thread-1', emailDate: '2024-01-01T12:00:00Z', createdAt: '2024-01-01T12:00:00Z', labels: [], owner: 1, emailAddress: 1, messageId: 'msg-1', inReplyTo: null, references: '', ccAddresses: [], bccAddresses: [], bodyText: 'This is a test email body.', bodyHtml: '

This is a test email body.

', isAnswered: false, isPermanentlyDeleted: false, deletedAt: null, attachments: [], updatedAt: '2024-01-01T12:00:00Z', }; const mockLabel = { id: 1, owner: 1, name: 'Important', color: '#ef4444', createdAt: '2024-01-01T00:00:00Z', }; const mockContact = { id: 1, owner: 1, email: 'contact@example.com', name: 'Contact Name', useCount: 5, lastUsedAt: '2024-01-01T00:00:00Z', }; const mockAttachment = { id: 1, filename: 'document.pdf', contentType: 'application/pdf', size: 1024, url: 'https://example.com/document.pdf', createdAt: '2024-01-01T00:00:00Z', }; const mockUserEmailAddress = { id: 1, email_address: 'user@example.com', display_name: 'User', color: '#3b82f6', is_default: true, last_check_at: '2024-01-01T00:00:00Z', emails_processed_count: 100, }; const createWrapper = () => { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false }, }, }); return function Wrapper({ children }: { children: React.ReactNode }) { return React.createElement(QueryClientProvider, { client: queryClient }, children); }; }; describe('useStaffEmail hooks', () => { beforeEach(() => { vi.clearAllMocks(); }); describe('staffEmailKeys', () => { it('generates correct query keys', () => { expect(staffEmailKeys.all).toEqual(['staffEmail']); expect(staffEmailKeys.folders()).toEqual(['staffEmail', 'folders']); expect(staffEmailKeys.emails()).toEqual(['staffEmail', 'emails']); expect(staffEmailKeys.emailDetail(1)).toEqual(['staffEmail', 'emails', 'detail', 1]); expect(staffEmailKeys.emailThread('thread-1')).toEqual(['staffEmail', 'emails', 'thread', 'thread-1']); expect(staffEmailKeys.labels()).toEqual(['staffEmail', 'labels']); expect(staffEmailKeys.contacts('test')).toEqual(['staffEmail', 'contacts', 'test']); expect(staffEmailKeys.userEmailAddresses()).toEqual(['staffEmail', 'userEmailAddresses']); }); it('generates email list key with filters', () => { const filters = { folderId: 1, emailAddressId: 2, search: 'test' }; const key = staffEmailKeys.emailList(filters); expect(key).toContain('staffEmail'); expect(key).toContain('emails'); expect(key).toContain('list'); }); }); describe('useStaffEmailFolders', () => { it('fetches email folders', async () => { vi.mocked(staffEmailApi.getFolders).mockResolvedValueOnce([mockFolder]); const { result } = renderHook(() => useStaffEmailFolders(), { wrapper: createWrapper() }); await waitFor(() => expect(result.current.isSuccess).toBe(true)); expect(staffEmailApi.getFolders).toHaveBeenCalled(); expect(result.current.data).toEqual([mockFolder]); }); it('handles error when fetching folders', async () => { vi.mocked(staffEmailApi.getFolders).mockRejectedValueOnce(new Error('Failed to fetch folders')); const { result } = renderHook(() => useStaffEmailFolders(), { wrapper: createWrapper() }); await waitFor(() => expect(result.current.isError).toBe(true)); }); }); describe('useCreateStaffEmailFolder', () => { it('creates a new folder', async () => { const newFolder = { ...mockFolder, id: 2, name: 'Custom Folder' }; vi.mocked(staffEmailApi.createFolder).mockResolvedValueOnce(newFolder); const { result } = renderHook(() => useCreateStaffEmailFolder(), { wrapper: createWrapper() }); await act(async () => { await result.current.mutateAsync('Custom Folder'); }); expect(staffEmailApi.createFolder).toHaveBeenCalledWith('Custom Folder'); }); }); describe('useUpdateStaffEmailFolder', () => { it('updates a folder name', async () => { const updatedFolder = { ...mockFolder, name: 'Updated Name' }; vi.mocked(staffEmailApi.updateFolder).mockResolvedValueOnce(updatedFolder); const { result } = renderHook(() => useUpdateStaffEmailFolder(), { wrapper: createWrapper() }); await act(async () => { await result.current.mutateAsync({ id: 1, name: 'Updated Name' }); }); expect(staffEmailApi.updateFolder).toHaveBeenCalledWith(1, 'Updated Name'); }); }); describe('useDeleteStaffEmailFolder', () => { it('deletes a folder', async () => { vi.mocked(staffEmailApi.deleteFolder).mockResolvedValueOnce(undefined); const { result } = renderHook(() => useDeleteStaffEmailFolder(), { wrapper: createWrapper() }); await act(async () => { await result.current.mutateAsync(1); }); expect(staffEmailApi.deleteFolder).toHaveBeenCalledWith(1); }); }); describe('useStaffEmail', () => { it('fetches a single email by id', async () => { vi.mocked(staffEmailApi.getEmail).mockResolvedValueOnce(mockEmail); const { result } = renderHook(() => useStaffEmail(1), { wrapper: createWrapper() }); await waitFor(() => expect(result.current.isSuccess).toBe(true)); expect(staffEmailApi.getEmail).toHaveBeenCalledWith(1); expect(result.current.data).toEqual(mockEmail); }); it('does not fetch when id is undefined', () => { const { result } = renderHook(() => useStaffEmail(undefined), { wrapper: createWrapper() }); expect(result.current.fetchStatus).toBe('idle'); expect(staffEmailApi.getEmail).not.toHaveBeenCalled(); }); }); describe('useStaffEmailThread', () => { it('fetches email thread', async () => { vi.mocked(staffEmailApi.getEmailThread).mockResolvedValueOnce([mockEmail]); const { result } = renderHook(() => useStaffEmailThread('thread-1'), { wrapper: createWrapper() }); await waitFor(() => expect(result.current.isSuccess).toBe(true)); expect(staffEmailApi.getEmailThread).toHaveBeenCalledWith('thread-1'); expect(result.current.data).toEqual([mockEmail]); }); it('does not fetch when threadId is undefined', () => { const { result } = renderHook(() => useStaffEmailThread(undefined), { wrapper: createWrapper() }); expect(result.current.fetchStatus).toBe('idle'); expect(staffEmailApi.getEmailThread).not.toHaveBeenCalled(); }); }); describe('useStaffEmailLabels', () => { it('fetches email labels', async () => { vi.mocked(staffEmailApi.getLabels).mockResolvedValueOnce([mockLabel]); const { result } = renderHook(() => useStaffEmailLabels(), { wrapper: createWrapper() }); await waitFor(() => expect(result.current.isSuccess).toBe(true)); expect(staffEmailApi.getLabels).toHaveBeenCalled(); expect(result.current.data).toEqual([mockLabel]); }); }); describe('useCreateLabel', () => { it('creates a new label', async () => { const newLabel = { ...mockLabel, id: 2, name: 'Work', color: '#10b981' }; vi.mocked(staffEmailApi.createLabel).mockResolvedValueOnce(newLabel); const { result } = renderHook(() => useCreateLabel(), { wrapper: createWrapper() }); await act(async () => { await result.current.mutateAsync({ name: 'Work', color: '#10b981' }); }); expect(staffEmailApi.createLabel).toHaveBeenCalledWith('Work', '#10b981'); }); }); describe('useUpdateLabel', () => { it('updates a label', async () => { const updatedLabel = { ...mockLabel, name: 'Updated Label' }; vi.mocked(staffEmailApi.updateLabel).mockResolvedValueOnce(updatedLabel); const { result } = renderHook(() => useUpdateLabel(), { wrapper: createWrapper() }); await act(async () => { await result.current.mutateAsync({ id: 1, data: { name: 'Updated Label' } }); }); expect(staffEmailApi.updateLabel).toHaveBeenCalledWith(1, { name: 'Updated Label' }); }); }); describe('useDeleteLabel', () => { it('deletes a label', async () => { vi.mocked(staffEmailApi.deleteLabel).mockResolvedValueOnce(undefined); const { result } = renderHook(() => useDeleteLabel(), { wrapper: createWrapper() }); await act(async () => { await result.current.mutateAsync(1); }); expect(staffEmailApi.deleteLabel).toHaveBeenCalledWith(1); }); }); describe('useAddLabelToEmail', () => { it('adds label to email', async () => { vi.mocked(staffEmailApi.addLabelToEmail).mockResolvedValueOnce(undefined); const { result } = renderHook(() => useAddLabelToEmail(), { wrapper: createWrapper() }); await act(async () => { await result.current.mutateAsync({ emailId: 1, labelId: 2 }); }); expect(staffEmailApi.addLabelToEmail).toHaveBeenCalledWith(1, 2); }); }); describe('useRemoveLabelFromEmail', () => { it('removes label from email', async () => { vi.mocked(staffEmailApi.removeLabelFromEmail).mockResolvedValueOnce(undefined); const { result } = renderHook(() => useRemoveLabelFromEmail(), { wrapper: createWrapper() }); await act(async () => { await result.current.mutateAsync({ emailId: 1, labelId: 2 }); }); expect(staffEmailApi.removeLabelFromEmail).toHaveBeenCalledWith(1, 2); }); }); describe('useCreateDraft', () => { it('creates a draft email', async () => { vi.mocked(staffEmailApi.createDraft).mockResolvedValueOnce(mockEmail); const draftData = { emailAddressId: 1, toAddresses: ['recipient@example.com'], subject: 'Test Draft', bodyText: 'Draft body', }; const { result } = renderHook(() => useCreateDraft(), { wrapper: createWrapper() }); await act(async () => { await result.current.mutateAsync(draftData); }); expect(staffEmailApi.createDraft).toHaveBeenCalledWith(draftData); }); }); describe('useUpdateDraft', () => { it('updates a draft email', async () => { vi.mocked(staffEmailApi.updateDraft).mockResolvedValueOnce(mockEmail); const { result } = renderHook(() => useUpdateDraft(), { wrapper: createWrapper() }); await act(async () => { await result.current.mutateAsync({ id: 1, data: { subject: 'Updated Subject' } }); }); expect(staffEmailApi.updateDraft).toHaveBeenCalledWith(1, { subject: 'Updated Subject' }); }); }); describe('useDeleteDraft', () => { it('deletes a draft', async () => { vi.mocked(staffEmailApi.deleteDraft).mockResolvedValueOnce(undefined); const { result } = renderHook(() => useDeleteDraft(), { wrapper: createWrapper() }); await act(async () => { await result.current.mutateAsync(1); }); expect(staffEmailApi.deleteDraft).toHaveBeenCalledWith(1); }); }); describe('useSendEmail', () => { it('sends an email', async () => { vi.mocked(staffEmailApi.sendEmail).mockResolvedValueOnce(mockEmail); const { result } = renderHook(() => useSendEmail(), { wrapper: createWrapper() }); await act(async () => { await result.current.mutateAsync(1); }); expect(staffEmailApi.sendEmail).toHaveBeenCalledWith(1); }); }); describe('useReplyToEmail', () => { it('replies to an email', async () => { vi.mocked(staffEmailApi.replyToEmail).mockResolvedValueOnce(mockEmail); const replyData = { bodyText: 'Reply body', bodyHtml: '

Reply body

', replyAll: false, }; const { result } = renderHook(() => useReplyToEmail(), { wrapper: createWrapper() }); await act(async () => { await result.current.mutateAsync({ id: 1, data: replyData }); }); expect(staffEmailApi.replyToEmail).toHaveBeenCalledWith(1, replyData); }); }); describe('useForwardEmail', () => { it('forwards an email', async () => { vi.mocked(staffEmailApi.forwardEmail).mockResolvedValueOnce(mockEmail); const forwardData = { toAddresses: ['forward@example.com'], bodyText: 'Forwarding this email', }; const { result } = renderHook(() => useForwardEmail(), { wrapper: createWrapper() }); await act(async () => { await result.current.mutateAsync({ id: 1, data: forwardData }); }); expect(staffEmailApi.forwardEmail).toHaveBeenCalledWith(1, forwardData); }); }); describe('useMarkAsRead', () => { it('marks email as read', async () => { vi.mocked(staffEmailApi.markAsRead).mockResolvedValueOnce(undefined); const { result } = renderHook(() => useMarkAsRead(), { wrapper: createWrapper() }); await act(async () => { await result.current.mutateAsync(1); }); expect(staffEmailApi.markAsRead).toHaveBeenCalledWith(1); }); }); describe('useMarkAsUnread', () => { it('marks email as unread', async () => { vi.mocked(staffEmailApi.markAsUnread).mockResolvedValueOnce(undefined); const { result } = renderHook(() => useMarkAsUnread(), { wrapper: createWrapper() }); await act(async () => { await result.current.mutateAsync(1); }); expect(staffEmailApi.markAsUnread).toHaveBeenCalledWith(1); }); }); describe('useStarEmail', () => { it('stars an email', async () => { vi.mocked(staffEmailApi.starEmail).mockResolvedValueOnce(undefined); const { result } = renderHook(() => useStarEmail(), { wrapper: createWrapper() }); await act(async () => { await result.current.mutateAsync(1); }); expect(staffEmailApi.starEmail).toHaveBeenCalledWith(1); }); }); describe('useUnstarEmail', () => { it('unstars an email', async () => { vi.mocked(staffEmailApi.unstarEmail).mockResolvedValueOnce(undefined); const { result } = renderHook(() => useUnstarEmail(), { wrapper: createWrapper() }); await act(async () => { await result.current.mutateAsync(1); }); expect(staffEmailApi.unstarEmail).toHaveBeenCalledWith(1); }); }); describe('useArchiveEmail', () => { it('archives an email', async () => { vi.mocked(staffEmailApi.archiveEmail).mockResolvedValueOnce(undefined); const { result } = renderHook(() => useArchiveEmail(), { wrapper: createWrapper() }); await act(async () => { await result.current.mutateAsync(1); }); expect(staffEmailApi.archiveEmail).toHaveBeenCalledWith(1); }); }); describe('useTrashEmail', () => { it('moves email to trash', async () => { vi.mocked(staffEmailApi.trashEmail).mockResolvedValueOnce(undefined); const { result } = renderHook(() => useTrashEmail(), { wrapper: createWrapper() }); await act(async () => { await result.current.mutateAsync(1); }); expect(staffEmailApi.trashEmail).toHaveBeenCalledWith(1); }); }); describe('useRestoreEmail', () => { it('restores an email from trash', async () => { vi.mocked(staffEmailApi.restoreEmail).mockResolvedValueOnce(undefined); const { result } = renderHook(() => useRestoreEmail(), { wrapper: createWrapper() }); await act(async () => { await result.current.mutateAsync(1); }); expect(staffEmailApi.restoreEmail).toHaveBeenCalledWith(1); }); }); describe('usePermanentlyDeleteEmail', () => { it('permanently deletes an email', async () => { vi.mocked(staffEmailApi.permanentlyDeleteEmail).mockResolvedValueOnce(undefined); const { result } = renderHook(() => usePermanentlyDeleteEmail(), { wrapper: createWrapper() }); await act(async () => { await result.current.mutateAsync(1); }); expect(staffEmailApi.permanentlyDeleteEmail).toHaveBeenCalledWith(1); }); }); describe('useMoveEmails', () => { it('moves emails to a folder', async () => { vi.mocked(staffEmailApi.moveEmails).mockResolvedValueOnce(undefined); const moveData = { emailIds: [1, 2, 3], folderId: 2 }; const { result } = renderHook(() => useMoveEmails(), { wrapper: createWrapper() }); await act(async () => { await result.current.mutateAsync(moveData); }); expect(staffEmailApi.moveEmails).toHaveBeenCalledWith(moveData); }); }); describe('useBulkEmailAction', () => { it('performs bulk action on emails', async () => { vi.mocked(staffEmailApi.bulkAction).mockResolvedValueOnce(undefined); const bulkData = { emailIds: [1, 2, 3], action: 'mark_read' as const }; const { result } = renderHook(() => useBulkEmailAction(), { wrapper: createWrapper() }); await act(async () => { await result.current.mutateAsync(bulkData); }); expect(staffEmailApi.bulkAction).toHaveBeenCalledWith(bulkData); }); }); describe('useContactSearch', () => { it('searches contacts with query', async () => { vi.mocked(staffEmailApi.searchContacts).mockResolvedValueOnce([mockContact]); const { result } = renderHook(() => useContactSearch('test'), { wrapper: createWrapper() }); await waitFor(() => expect(result.current.isSuccess).toBe(true)); expect(staffEmailApi.searchContacts).toHaveBeenCalledWith('test'); expect(result.current.data).toEqual([mockContact]); }); it('does not search with query less than 2 characters', () => { const { result } = renderHook(() => useContactSearch('t'), { wrapper: createWrapper() }); expect(result.current.fetchStatus).toBe('idle'); expect(staffEmailApi.searchContacts).not.toHaveBeenCalled(); }); }); describe('useUploadAttachment', () => { it('uploads an attachment', async () => { vi.mocked(staffEmailApi.uploadAttachment).mockResolvedValueOnce(mockAttachment); const file = new File(['test content'], 'test.pdf', { type: 'application/pdf' }); const { result } = renderHook(() => useUploadAttachment(), { wrapper: createWrapper() }); await act(async () => { await result.current.mutateAsync({ file, emailId: 1 }); }); expect(staffEmailApi.uploadAttachment).toHaveBeenCalledWith(file, 1); }); it('uploads attachment without email id', async () => { vi.mocked(staffEmailApi.uploadAttachment).mockResolvedValueOnce(mockAttachment); const file = new File(['test content'], 'test.pdf', { type: 'application/pdf' }); const { result } = renderHook(() => useUploadAttachment(), { wrapper: createWrapper() }); await act(async () => { await result.current.mutateAsync({ file }); }); expect(staffEmailApi.uploadAttachment).toHaveBeenCalledWith(file, undefined); }); }); describe('useDeleteAttachment', () => { it('deletes an attachment', async () => { vi.mocked(staffEmailApi.deleteAttachment).mockResolvedValueOnce(undefined); const { result } = renderHook(() => useDeleteAttachment(), { wrapper: createWrapper() }); await act(async () => { await result.current.mutateAsync(1); }); expect(staffEmailApi.deleteAttachment).toHaveBeenCalledWith(1); }); }); describe('useSyncEmails', () => { it('syncs emails', async () => { vi.mocked(staffEmailApi.syncEmails).mockResolvedValueOnce({ success: true, message: 'Synced' }); const { result } = renderHook(() => useSyncEmails(), { wrapper: createWrapper() }); await act(async () => { await result.current.mutateAsync(); }); expect(staffEmailApi.syncEmails).toHaveBeenCalled(); }); }); describe('useFullSyncEmails', () => { it('performs full email sync', async () => { vi.mocked(staffEmailApi.fullSyncEmails).mockResolvedValueOnce({ status: 'started', tasks: [{ email_address: 'user@example.com', task_id: 'task-1' }], }); const { result } = renderHook(() => useFullSyncEmails(), { wrapper: createWrapper() }); await act(async () => { await result.current.mutateAsync(); }); expect(staffEmailApi.fullSyncEmails).toHaveBeenCalled(); }); }); describe('useUserEmailAddresses', () => { it('fetches user email addresses', async () => { vi.mocked(staffEmailApi.getUserEmailAddresses).mockResolvedValueOnce([mockUserEmailAddress]); const { result } = renderHook(() => useUserEmailAddresses(), { wrapper: createWrapper() }); await waitFor(() => expect(result.current.isSuccess).toBe(true)); expect(staffEmailApi.getUserEmailAddresses).toHaveBeenCalled(); expect(result.current.data).toEqual([mockUserEmailAddress]); }); }); });