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'; // Mock the tickets API module vi.mock('../../api/tickets', () => ({ getTickets: vi.fn(), getTicket: vi.fn(), createTicket: vi.fn(), updateTicket: vi.fn(), deleteTicket: vi.fn(), getTicketComments: vi.fn(), createTicketComment: vi.fn(), getTicketTemplates: vi.fn(), getTicketTemplate: vi.fn(), getCannedResponses: vi.fn(), refreshTicketEmails: vi.fn(), })); import { useTickets, useTicket, useCreateTicket, useUpdateTicket, useDeleteTicket, useTicketComments, useCreateTicketComment, useTicketTemplates, useTicketTemplate, useCannedResponses, useRefreshTicketEmails, } from '../useTickets'; import * as ticketsApi from '../../api/tickets'; // Create wrapper with QueryClient 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('useTickets hooks', () => { beforeEach(() => { vi.clearAllMocks(); }); describe('useTickets', () => { it('fetches tickets and transforms snake_case to camelCase', async () => { const mockTickets = [ { id: 1, tenant: 5, creator: 10, creator_email: 'creator@example.com', creator_full_name: 'John Doe', assignee: 20, assignee_email: 'assignee@example.com', assignee_full_name: 'Jane Smith', ticket_type: 'SUPPORT', status: 'OPEN', priority: 'HIGH', subject: 'Test Ticket', description: 'Test description', category: 'BILLING', related_appointment_id: 100, due_at: '2025-12-08T12:00:00Z', first_response_at: '2025-12-07T10:00:00Z', is_overdue: false, created_at: '2025-12-07T09:00:00Z', updated_at: '2025-12-07T11:00:00Z', resolved_at: null, comments: 3, }, ]; vi.mocked(ticketsApi.getTickets).mockResolvedValue(mockTickets as any); const { result } = renderHook(() => useTickets(), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(ticketsApi.getTickets).toHaveBeenCalledWith({}); expect(result.current.data).toHaveLength(1); expect(result.current.data?.[0]).toEqual({ id: '1', tenant: '5', creator: '10', creatorEmail: 'creator@example.com', creatorFullName: 'John Doe', assignee: '20', assigneeEmail: 'assignee@example.com', assigneeFullName: 'Jane Smith', ticketType: 'SUPPORT', status: 'OPEN', priority: 'HIGH', subject: 'Test Ticket', description: 'Test description', category: 'BILLING', relatedAppointmentId: 100, dueAt: '2025-12-08T12:00:00Z', firstResponseAt: '2025-12-07T10:00:00Z', isOverdue: false, createdAt: '2025-12-07T09:00:00Z', updatedAt: '2025-12-07T11:00:00Z', resolvedAt: null, comments: 3, }); }); it('handles undefined optional fields correctly', async () => { const mockTickets = [ { id: 2, tenant: null, creator: 15, creator_email: 'test@example.com', creator_full_name: 'Test User', assignee: null, assignee_email: null, assignee_full_name: null, ticket_type: 'FEATURE_REQUEST', status: 'NEW', priority: 'LOW', subject: 'Feature Request', description: 'New feature', category: 'GENERAL', related_appointment_id: null, due_at: null, first_response_at: null, is_overdue: false, created_at: '2025-12-07T09:00:00Z', updated_at: '2025-12-07T09:00:00Z', resolved_at: null, comments: 0, }, ]; vi.mocked(ticketsApi.getTickets).mockResolvedValue(mockTickets as any); const { result } = renderHook(() => useTickets(), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(result.current.data?.[0]).toMatchObject({ id: '2', tenant: undefined, assignee: undefined, relatedAppointmentId: undefined, }); }); it('applies status filter', async () => { vi.mocked(ticketsApi.getTickets).mockResolvedValue([]); renderHook(() => useTickets({ status: 'OPEN' }), { wrapper: createWrapper(), }); await waitFor(() => { expect(ticketsApi.getTickets).toHaveBeenCalledWith({ status: 'OPEN' }); }); }); it('applies priority filter', async () => { vi.mocked(ticketsApi.getTickets).mockResolvedValue([]); renderHook(() => useTickets({ priority: 'HIGH' }), { wrapper: createWrapper(), }); await waitFor(() => { expect(ticketsApi.getTickets).toHaveBeenCalledWith({ priority: 'HIGH' }); }); }); it('applies category filter', async () => { vi.mocked(ticketsApi.getTickets).mockResolvedValue([]); renderHook(() => useTickets({ category: 'BILLING' }), { wrapper: createWrapper(), }); await waitFor(() => { expect(ticketsApi.getTickets).toHaveBeenCalledWith({ category: 'BILLING' }); }); }); it('applies ticketType filter', async () => { vi.mocked(ticketsApi.getTickets).mockResolvedValue([]); renderHook(() => useTickets({ ticketType: 'SUPPORT' }), { wrapper: createWrapper(), }); await waitFor(() => { expect(ticketsApi.getTickets).toHaveBeenCalledWith({ ticketType: 'SUPPORT' }); }); }); it('applies assignee filter', async () => { vi.mocked(ticketsApi.getTickets).mockResolvedValue([]); renderHook(() => useTickets({ assignee: '42' }), { wrapper: createWrapper(), }); await waitFor(() => { expect(ticketsApi.getTickets).toHaveBeenCalledWith({ assignee: '42' }); }); }); it('applies multiple filters', async () => { vi.mocked(ticketsApi.getTickets).mockResolvedValue([]); renderHook(() => useTickets({ status: 'IN_PROGRESS', priority: 'HIGH', assignee: '10' }), { wrapper: createWrapper(), }); await waitFor(() => { expect(ticketsApi.getTickets).toHaveBeenCalledWith({ status: 'IN_PROGRESS', priority: 'HIGH', assignee: '10', }); }); }); }); describe('useTicket', () => { it('fetches single ticket by id and transforms data', async () => { const mockTicket = { id: 5, tenant: 3, creator: 12, creator_email: 'user@example.com', creator_full_name: 'User Name', assignee: 25, assignee_email: 'staff@example.com', assignee_full_name: 'Staff Member', ticket_type: 'BUG', status: 'IN_PROGRESS', priority: 'MEDIUM', subject: 'Bug Report', description: 'Bug details', category: 'TECHNICAL', related_appointment_id: 200, due_at: '2025-12-10T12:00:00Z', first_response_at: '2025-12-07T11:00:00Z', is_overdue: false, created_at: '2025-12-07T10:00:00Z', updated_at: '2025-12-07T12:00:00Z', resolved_at: null, comments: 5, }; vi.mocked(ticketsApi.getTicket).mockResolvedValue(mockTicket as any); const { result } = renderHook(() => useTicket('5'), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(ticketsApi.getTicket).toHaveBeenCalledWith('5'); expect(result.current.data).toEqual({ id: '5', tenant: '3', creator: '12', creatorEmail: 'user@example.com', creatorFullName: 'User Name', assignee: '25', assigneeEmail: 'staff@example.com', assigneeFullName: 'Staff Member', ticketType: 'BUG', status: 'IN_PROGRESS', priority: 'MEDIUM', subject: 'Bug Report', description: 'Bug details', category: 'TECHNICAL', relatedAppointmentId: 200, dueAt: '2025-12-10T12:00:00Z', firstResponseAt: '2025-12-07T11:00:00Z', isOverdue: false, createdAt: '2025-12-07T10:00:00Z', updatedAt: '2025-12-07T12:00:00Z', resolvedAt: null, comments: 5, }); }); it('does not fetch when id is undefined', async () => { const { result } = renderHook(() => useTicket(undefined), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isLoading).toBe(false); }); expect(ticketsApi.getTicket).not.toHaveBeenCalled(); }); it('does not fetch when id is empty string', async () => { const { result } = renderHook(() => useTicket(''), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isLoading).toBe(false); }); expect(ticketsApi.getTicket).not.toHaveBeenCalled(); }); }); describe('useCreateTicket', () => { it('creates ticket with camelCase to snake_case transformation', async () => { const mockResponse = { id: 10 }; vi.mocked(ticketsApi.createTicket).mockResolvedValue(mockResponse as any); const { result } = renderHook(() => useCreateTicket(), { wrapper: createWrapper(), }); await act(async () => { await result.current.mutateAsync({ ticketType: 'SUPPORT', subject: 'New Ticket', description: 'Ticket description', priority: 'HIGH', status: 'NEW', category: 'BILLING', assignee: '15', }); }); expect(ticketsApi.createTicket).toHaveBeenCalledWith({ ticketType: 'SUPPORT', subject: 'New Ticket', description: 'Ticket description', priority: 'HIGH', status: 'NEW', category: 'BILLING', assignee: '15', ticket_type: 'SUPPORT', }); }); it('sets assignee to null when not provided', async () => { const mockResponse = { id: 11 }; vi.mocked(ticketsApi.createTicket).mockResolvedValue(mockResponse as any); const { result } = renderHook(() => useCreateTicket(), { wrapper: createWrapper(), }); await act(async () => { await result.current.mutateAsync({ ticketType: 'BUG', subject: 'Bug Report', description: 'Description', priority: 'LOW', status: 'NEW', category: 'TECHNICAL', }); }); expect(ticketsApi.createTicket).toHaveBeenCalledWith( expect.objectContaining({ assignee: null, }) ); }); it('invalidates tickets query on success', async () => { const mockResponse = { id: 12 }; vi.mocked(ticketsApi.createTicket).mockResolvedValue(mockResponse as any); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false }, }, }); const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries'); const wrapper = ({ children }: { children: React.ReactNode }) => React.createElement(QueryClientProvider, { client: queryClient }, children); const { result } = renderHook(() => useCreateTicket(), { wrapper }); await act(async () => { await result.current.mutateAsync({ ticketType: 'SUPPORT', subject: 'Test', description: 'Test', priority: 'MEDIUM', status: 'NEW', category: 'GENERAL', }); }); expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['tickets'] }); }); }); describe('useUpdateTicket', () => { it('updates ticket with field transformation', async () => { const mockResponse = { id: 20 }; vi.mocked(ticketsApi.updateTicket).mockResolvedValue(mockResponse as any); const { result } = renderHook(() => useUpdateTicket(), { wrapper: createWrapper(), }); await act(async () => { await result.current.mutateAsync({ id: '20', updates: { subject: 'Updated Subject', priority: 'HIGH', ticketType: 'SUPPORT', assignee: '30', }, }); }); expect(ticketsApi.updateTicket).toHaveBeenCalledWith('20', { subject: 'Updated Subject', priority: 'HIGH', ticketType: 'SUPPORT', assignee: '30', ticket_type: 'SUPPORT', }); }); it('sets assignee to null when not provided', async () => { const mockResponse = { id: 21 }; vi.mocked(ticketsApi.updateTicket).mockResolvedValue(mockResponse as any); const { result } = renderHook(() => useUpdateTicket(), { wrapper: createWrapper(), }); await act(async () => { await result.current.mutateAsync({ id: '21', updates: { status: 'RESOLVED', }, }); }); expect(ticketsApi.updateTicket).toHaveBeenCalledWith('21', { status: 'RESOLVED', ticket_type: undefined, assignee: null, }); }); it('invalidates tickets queries on success', async () => { const mockResponse = { id: 22 }; vi.mocked(ticketsApi.updateTicket).mockResolvedValue(mockResponse as any); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false }, }, }); const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries'); const wrapper = ({ children }: { children: React.ReactNode }) => React.createElement(QueryClientProvider, { client: queryClient }, children); const { result } = renderHook(() => useUpdateTicket(), { wrapper }); await act(async () => { await result.current.mutateAsync({ id: '22', updates: { status: 'IN_PROGRESS' }, }); }); expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['tickets'] }); expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['tickets', '22'] }); }); }); describe('useDeleteTicket', () => { it('deletes ticket by id', async () => { vi.mocked(ticketsApi.deleteTicket).mockResolvedValue(undefined); const { result } = renderHook(() => useDeleteTicket(), { wrapper: createWrapper(), }); await act(async () => { const returnedId = await result.current.mutateAsync('30'); expect(returnedId).toBe('30'); }); expect(ticketsApi.deleteTicket).toHaveBeenCalledWith('30'); }); it('invalidates tickets query on success', async () => { vi.mocked(ticketsApi.deleteTicket).mockResolvedValue(undefined); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false }, }, }); const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries'); const wrapper = ({ children }: { children: React.ReactNode }) => React.createElement(QueryClientProvider, { client: queryClient }, children); const { result } = renderHook(() => useDeleteTicket(), { wrapper }); await act(async () => { await result.current.mutateAsync('31'); }); expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['tickets'] }); }); }); describe('useTicketComments', () => { it('fetches comments and transforms snake_case to camelCase', async () => { const mockComments = [ { id: 100, ticket: 50, author: 15, comment_text: 'This is a comment', is_internal: false, created_at: '2025-12-07T10:00:00Z', }, { id: 101, ticket: 50, author: 20, comment_text: 'Internal note', is_internal: true, created_at: '2025-12-07T11:00:00Z', }, ]; vi.mocked(ticketsApi.getTicketComments).mockResolvedValue(mockComments as any); const { result } = renderHook(() => useTicketComments('50'), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(ticketsApi.getTicketComments).toHaveBeenCalledWith('50'); expect(result.current.data).toHaveLength(2); expect(result.current.data?.[0]).toMatchObject({ id: '100', ticket: '50', author: '15', commentText: 'This is a comment', isInternal: false, createdAt: expect.any(String), }); expect(result.current.data?.[1]).toMatchObject({ id: '101', ticket: '50', author: '20', commentText: 'Internal note', isInternal: true, }); }); it('transforms created_at to ISO string', async () => { const mockComments = [ { id: 102, ticket: 51, author: 16, comment_text: 'Test comment', is_internal: false, created_at: '2025-12-07T10:30:00Z', }, ]; vi.mocked(ticketsApi.getTicketComments).mockResolvedValue(mockComments as any); const { result } = renderHook(() => useTicketComments('51'), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(result.current.data?.[0].createdAt).toBe('2025-12-07T10:30:00.000Z'); }); it('returns empty array when ticketId is undefined', async () => { const { result } = renderHook(() => useTicketComments(undefined), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isLoading).toBe(false); }); expect(ticketsApi.getTicketComments).not.toHaveBeenCalled(); }); it('does not fetch when ticketId is empty string', async () => { const { result } = renderHook(() => useTicketComments(''), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isLoading).toBe(false); }); expect(ticketsApi.getTicketComments).not.toHaveBeenCalled(); }); }); describe('useCreateTicketComment', () => { it('creates comment with field transformation', async () => { const mockResponse = { id: 105 }; vi.mocked(ticketsApi.createTicketComment).mockResolvedValue(mockResponse as any); const { result } = renderHook(() => useCreateTicketComment(), { wrapper: createWrapper(), }); await act(async () => { await result.current.mutateAsync({ ticketId: '60', commentData: { commentText: 'New comment', isInternal: false, }, }); }); expect(ticketsApi.createTicketComment).toHaveBeenCalledWith('60', { commentText: 'New comment', isInternal: false, comment_text: 'New comment', is_internal: false, }); }); it('handles internal comments', async () => { const mockResponse = { id: 106 }; vi.mocked(ticketsApi.createTicketComment).mockResolvedValue(mockResponse as any); const { result } = renderHook(() => useCreateTicketComment(), { wrapper: createWrapper(), }); await act(async () => { await result.current.mutateAsync({ ticketId: '61', commentData: { commentText: 'Internal note', isInternal: true, }, }); }); expect(ticketsApi.createTicketComment).toHaveBeenCalledWith('61', { commentText: 'Internal note', isInternal: true, comment_text: 'Internal note', is_internal: true, }); }); it('invalidates queries on success', async () => { const mockResponse = { id: 107 }; vi.mocked(ticketsApi.createTicketComment).mockResolvedValue(mockResponse as any); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false }, }, }); const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries'); const wrapper = ({ children }: { children: React.ReactNode }) => React.createElement(QueryClientProvider, { client: queryClient }, children); const { result } = renderHook(() => useCreateTicketComment(), { wrapper }); await act(async () => { await result.current.mutateAsync({ ticketId: '62', commentData: { commentText: 'Comment', isInternal: false, }, }); }); expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['ticketComments', '62'] }); expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['tickets', '62'] }); }); }); describe('useTicketTemplates', () => { it('fetches templates and transforms snake_case to camelCase', async () => { const mockTemplates = [ { id: 200, tenant: 10, name: 'Support Template', description: 'Template for support tickets', ticket_type: 'SUPPORT', category: 'GENERAL', default_priority: 'MEDIUM', subject_template: 'Support: {{issue}}', description_template: 'Issue: {{description}}', is_active: true, created_at: '2025-12-01T10:00:00Z', }, ]; vi.mocked(ticketsApi.getTicketTemplates).mockResolvedValue(mockTemplates as any); const { result } = renderHook(() => useTicketTemplates(), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(ticketsApi.getTicketTemplates).toHaveBeenCalled(); expect(result.current.data).toHaveLength(1); expect(result.current.data?.[0]).toEqual({ id: '200', tenant: '10', name: 'Support Template', description: 'Template for support tickets', ticketType: 'SUPPORT', category: 'GENERAL', defaultPriority: 'MEDIUM', subjectTemplate: 'Support: {{issue}}', descriptionTemplate: 'Issue: {{description}}', isActive: true, createdAt: '2025-12-01T10:00:00Z', }); }); it('handles undefined tenant', async () => { const mockTemplates = [ { id: 201, tenant: null, name: 'Global Template', description: 'Global template', ticket_type: 'BUG', category: 'TECHNICAL', default_priority: 'HIGH', subject_template: 'Bug: {{title}}', description_template: 'Details: {{details}}', is_active: true, created_at: '2025-12-01T10:00:00Z', }, ]; vi.mocked(ticketsApi.getTicketTemplates).mockResolvedValue(mockTemplates as any); const { result } = renderHook(() => useTicketTemplates(), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(result.current.data?.[0].tenant).toBeUndefined(); }); }); describe('useTicketTemplate', () => { it('fetches single template by id and transforms data', async () => { const mockTemplate = { id: 202, tenant: 11, name: 'Feature Template', description: 'Template for features', ticket_type: 'FEATURE_REQUEST', category: 'GENERAL', default_priority: 'LOW', subject_template: 'Feature: {{name}}', description_template: 'Feature request: {{details}}', is_active: true, created_at: '2025-12-01T11:00:00Z', }; vi.mocked(ticketsApi.getTicketTemplate).mockResolvedValue(mockTemplate as any); const { result } = renderHook(() => useTicketTemplate('202'), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(ticketsApi.getTicketTemplate).toHaveBeenCalledWith('202'); expect(result.current.data).toEqual({ id: '202', tenant: '11', name: 'Feature Template', description: 'Template for features', ticketType: 'FEATURE_REQUEST', category: 'GENERAL', defaultPriority: 'LOW', subjectTemplate: 'Feature: {{name}}', descriptionTemplate: 'Feature request: {{details}}', isActive: true, createdAt: '2025-12-01T11:00:00Z', }); }); it('does not fetch when id is undefined', async () => { const { result } = renderHook(() => useTicketTemplate(undefined), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isLoading).toBe(false); }); expect(ticketsApi.getTicketTemplate).not.toHaveBeenCalled(); }); it('does not fetch when id is empty string', async () => { const { result } = renderHook(() => useTicketTemplate(''), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isLoading).toBe(false); }); expect(ticketsApi.getTicketTemplate).not.toHaveBeenCalled(); }); }); describe('useCannedResponses', () => { it('fetches canned responses and transforms snake_case to camelCase', async () => { const mockResponses = [ { id: 300, tenant: 12, title: 'Thank You', content: 'Thank you for contacting us!', category: 'GENERAL', is_active: true, use_count: 42, created_by: 20, created_at: '2025-11-01T10:00:00Z', }, { id: 301, tenant: 12, title: 'Billing Info', content: 'Here is billing information...', category: 'BILLING', is_active: true, use_count: 15, created_by: null, created_at: '2025-11-02T10:00:00Z', }, ]; vi.mocked(ticketsApi.getCannedResponses).mockResolvedValue(mockResponses as any); const { result } = renderHook(() => useCannedResponses(), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(ticketsApi.getCannedResponses).toHaveBeenCalled(); expect(result.current.data).toHaveLength(2); expect(result.current.data?.[0]).toEqual({ id: '300', tenant: '12', title: 'Thank You', content: 'Thank you for contacting us!', category: 'GENERAL', isActive: true, useCount: 42, createdBy: '20', createdAt: '2025-11-01T10:00:00Z', }); expect(result.current.data?.[1].createdBy).toBeUndefined(); }); it('handles null tenant and createdBy', async () => { const mockResponses = [ { id: 302, tenant: null, title: 'Global Response', content: 'This is a global response', category: 'GENERAL', is_active: true, use_count: 0, created_by: null, created_at: '2025-11-03T10:00:00Z', }, ]; vi.mocked(ticketsApi.getCannedResponses).mockResolvedValue(mockResponses as any); const { result } = renderHook(() => useCannedResponses(), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(result.current.data?.[0]).toMatchObject({ id: '302', tenant: undefined, createdBy: undefined, }); }); }); describe('useRefreshTicketEmails', () => { it('calls refreshTicketEmails API', async () => { const mockResponse = { success: true, processed: 3, results: [ { address: 'support@example.com', display_name: 'Support', processed: 3, status: 'success', last_check_at: '2025-12-07T12:00:00Z', }, ], }; vi.mocked(ticketsApi.refreshTicketEmails).mockResolvedValue(mockResponse); const { result } = renderHook(() => useRefreshTicketEmails(), { wrapper: createWrapper(), }); await act(async () => { const response = await result.current.mutateAsync(); expect(response).toEqual(mockResponse); }); expect(ticketsApi.refreshTicketEmails).toHaveBeenCalled(); }); it('invalidates tickets query when emails are processed', async () => { const mockResponse = { success: true, processed: 2, results: [], }; vi.mocked(ticketsApi.refreshTicketEmails).mockResolvedValue(mockResponse); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false }, }, }); const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries'); const wrapper = ({ children }: { children: React.ReactNode }) => React.createElement(QueryClientProvider, { client: queryClient }, children); const { result } = renderHook(() => useRefreshTicketEmails(), { wrapper }); await act(async () => { await result.current.mutateAsync(); }); expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['tickets'] }); }); it('does not invalidate queries when no emails are processed', async () => { const mockResponse = { success: true, processed: 0, results: [], }; vi.mocked(ticketsApi.refreshTicketEmails).mockResolvedValue(mockResponse); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false }, }, }); const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries'); const wrapper = ({ children }: { children: React.ReactNode }) => React.createElement(QueryClientProvider, { client: queryClient }, children); const { result } = renderHook(() => useRefreshTicketEmails(), { wrapper }); await act(async () => { await result.current.mutateAsync(); }); expect(invalidateSpy).not.toHaveBeenCalled(); }); it('handles error responses', async () => { const mockResponse = { success: false, processed: 0, results: [ { address: null, status: 'error', error: 'Connection failed', message: 'Unable to connect to mail server', }, ], }; vi.mocked(ticketsApi.refreshTicketEmails).mockResolvedValue(mockResponse); const { result } = renderHook(() => useRefreshTicketEmails(), { wrapper: createWrapper(), }); await act(async () => { const response = await result.current.mutateAsync(); expect(response.success).toBe(false); expect(response.results[0].error).toBe('Connection failed'); }); }); }); });