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 apiClient vi.mock('../../api/client', () => ({ default: { get: vi.fn(), post: vi.fn(), patch: vi.fn(), delete: vi.fn(), }, })); import { useContractTemplates, useContractTemplate, useCreateContractTemplate, useUpdateContractTemplate, useDeleteContractTemplate, useDuplicateContractTemplate, usePreviewContractTemplate, useContracts, useContract, useCreateContract, useSendContract, useVoidContract, useResendContract, usePublicContract, useSignContract, useExportLegalPackage, } from '../useContracts'; import apiClient from '../../api/client'; // Create wrapper 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('useContracts hooks', () => { beforeEach(() => { vi.clearAllMocks(); }); // --- Contract Templates --- describe('useContractTemplates', () => { it('fetches all contract templates and transforms data', async () => { const mockTemplates = [ { id: 1, name: 'Service Agreement', description: 'Standard service agreement', content: '
Service terms...
', scope: 'CUSTOMER', status: 'ACTIVE', expires_after_days: 30, version: 1, version_notes: 'Initial version', services: [{ id: 10, name: 'Consultation' }], created_by: 5, created_by_name: 'John Doe', created_at: '2024-01-01T00:00:00Z', updated_at: '2024-01-02T00:00:00Z', }, { id: 2, name: 'NDA', description: null, content: 'Confidentiality terms...
', scope: 'APPOINTMENT', status: 'DRAFT', expires_after_days: null, version: 2, version_notes: null, services: [], created_by: null, created_by_name: null, created_at: '2024-01-03T00:00:00Z', updated_at: '2024-01-04T00:00:00Z', }, ]; vi.mocked(apiClient.get).mockResolvedValue({ data: mockTemplates }); const { result } = renderHook(() => useContractTemplates(), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(apiClient.get).toHaveBeenCalledWith('/contracts/templates/', { params: {} }); expect(result.current.data).toHaveLength(2); expect(result.current.data?.[0]).toEqual({ id: '1', name: 'Service Agreement', description: 'Standard service agreement', content: 'Service terms...
', scope: 'CUSTOMER', status: 'ACTIVE', expires_after_days: 30, version: 1, version_notes: 'Initial version', services: [{ id: 10, name: 'Consultation' }], created_by: '5', created_by_name: 'John Doe', created_at: '2024-01-01T00:00:00Z', updated_at: '2024-01-02T00:00:00Z', }); expect(result.current.data?.[1]).toEqual({ id: '2', name: 'NDA', description: '', content: 'Confidentiality terms...
', scope: 'APPOINTMENT', status: 'DRAFT', expires_after_days: null, version: 2, version_notes: '', services: [], created_by: null, created_by_name: null, created_at: '2024-01-03T00:00:00Z', updated_at: '2024-01-04T00:00:00Z', }); }); it('applies status filter', async () => { vi.mocked(apiClient.get).mockResolvedValue({ data: [] }); renderHook(() => useContractTemplates('ACTIVE'), { wrapper: createWrapper(), }); await waitFor(() => { expect(apiClient.get).toHaveBeenCalledWith('/contracts/templates/', { params: { status: 'ACTIVE' }, }); }); }); it('fetches without filter when status is undefined', async () => { vi.mocked(apiClient.get).mockResolvedValue({ data: [] }); renderHook(() => useContractTemplates(undefined), { wrapper: createWrapper(), }); await waitFor(() => { expect(apiClient.get).toHaveBeenCalledWith('/contracts/templates/', { params: {} }); }); }); }); describe('useContractTemplate', () => { it('fetches single contract template by id', async () => { const mockTemplate = { id: 1, name: 'Service Agreement', description: 'Standard service agreement', content: 'Service terms...
', scope: 'CUSTOMER', status: 'ACTIVE', expires_after_days: 30, version: 1, version_notes: 'Initial version', services: [{ id: 10, name: 'Consultation' }], created_by: 5, created_by_name: 'John Doe', created_at: '2024-01-01T00:00:00Z', updated_at: '2024-01-02T00:00:00Z', }; vi.mocked(apiClient.get).mockResolvedValue({ data: mockTemplate }); const { result } = renderHook(() => useContractTemplate('1'), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(apiClient.get).toHaveBeenCalledWith('/contracts/templates/1/'); expect(result.current.data).toEqual({ id: '1', name: 'Service Agreement', description: 'Standard service agreement', content: 'Service terms...
', scope: 'CUSTOMER', status: 'ACTIVE', expires_after_days: 30, version: 1, version_notes: 'Initial version', services: [{ id: 10, name: 'Consultation' }], created_by: '5', created_by_name: 'John Doe', created_at: '2024-01-01T00:00:00Z', updated_at: '2024-01-02T00:00:00Z', }); }); it('does not fetch when id is empty', async () => { const { result } = renderHook(() => useContractTemplate(''), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isLoading).toBe(false); }); expect(apiClient.get).not.toHaveBeenCalled(); }); it('handles null values in optional fields', async () => { const mockTemplate = { id: 2, name: 'NDA', description: null, content: 'NDA content
', scope: 'APPOINTMENT', status: 'DRAFT', expires_after_days: null, version: 1, version_notes: null, services: null, created_by: null, created_by_name: null, created_at: '2024-01-01T00:00:00Z', updated_at: '2024-01-01T00:00:00Z', }; vi.mocked(apiClient.get).mockResolvedValue({ data: mockTemplate }); const { result } = renderHook(() => useContractTemplate('2'), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(result.current.data?.description).toBe(''); expect(result.current.data?.version_notes).toBe(''); expect(result.current.data?.services).toEqual([]); expect(result.current.data?.created_by).toBeNull(); expect(result.current.data?.created_by_name).toBeNull(); }); }); describe('useCreateContractTemplate', () => { it('creates contract template', async () => { vi.mocked(apiClient.post).mockResolvedValue({ data: { id: 1 } }); const { result } = renderHook(() => useCreateContractTemplate(), { wrapper: createWrapper(), }); await act(async () => { await result.current.mutateAsync({ name: 'New Template', content: 'Template content
', scope: 'CUSTOMER', description: 'Test description', status: 'DRAFT', expires_after_days: 30, version_notes: 'Initial draft', services: ['1', '2'], }); }); expect(apiClient.post).toHaveBeenCalledWith('/contracts/templates/', { name: 'New Template', content: 'Template content
', scope: 'CUSTOMER', description: 'Test description', status: 'DRAFT', expires_after_days: 30, version_notes: 'Initial draft', services: ['1', '2'], }); }); it('creates template with minimal required fields', async () => { vi.mocked(apiClient.post).mockResolvedValue({ data: { id: 1 } }); const { result } = renderHook(() => useCreateContractTemplate(), { wrapper: createWrapper(), }); await act(async () => { await result.current.mutateAsync({ name: 'Minimal Template', content: 'Content
', scope: 'APPOINTMENT', }); }); expect(apiClient.post).toHaveBeenCalledWith('/contracts/templates/', { name: 'Minimal Template', content: 'Content
', scope: 'APPOINTMENT', }); }); }); describe('useUpdateContractTemplate', () => { it('updates contract template with partial data', async () => { vi.mocked(apiClient.patch).mockResolvedValue({ data: { id: 1 } }); const { result } = renderHook(() => useUpdateContractTemplate(), { wrapper: createWrapper(), }); await act(async () => { await result.current.mutateAsync({ id: '1', updates: { name: 'Updated Template', status: 'ACTIVE', }, }); }); expect(apiClient.patch).toHaveBeenCalledWith('/contracts/templates/1/', { name: 'Updated Template', status: 'ACTIVE', }); }); it('updates single field', async () => { vi.mocked(apiClient.patch).mockResolvedValue({ data: {} }); const { result } = renderHook(() => useUpdateContractTemplate(), { wrapper: createWrapper(), }); await act(async () => { await result.current.mutateAsync({ id: '1', updates: { expires_after_days: null }, }); }); expect(apiClient.patch).toHaveBeenCalledWith('/contracts/templates/1/', { expires_after_days: null, }); }); }); describe('useDeleteContractTemplate', () => { it('deletes contract template by id', async () => { vi.mocked(apiClient.delete).mockResolvedValue({}); const { result } = renderHook(() => useDeleteContractTemplate(), { wrapper: createWrapper(), }); await act(async () => { await result.current.mutateAsync('5'); }); expect(apiClient.delete).toHaveBeenCalledWith('/contracts/templates/5/'); }); }); describe('useDuplicateContractTemplate', () => { it('duplicates contract template', async () => { const mockDuplicate = { id: 2, name: 'Service Agreement (Copy)' }; vi.mocked(apiClient.post).mockResolvedValue({ data: mockDuplicate }); const { result } = renderHook(() => useDuplicateContractTemplate(), { wrapper: createWrapper(), }); let responseData; await act(async () => { responseData = await result.current.mutateAsync('1'); }); expect(apiClient.post).toHaveBeenCalledWith('/contracts/templates/1/duplicate/'); expect(responseData).toEqual(mockDuplicate); }); }); describe('usePreviewContractTemplate', () => { it('previews contract template with context', async () => { const mockPreview = { html: 'Preview with John Doe
' }; vi.mocked(apiClient.post).mockResolvedValue({ data: mockPreview }); const { result } = renderHook(() => usePreviewContractTemplate(), { wrapper: createWrapper(), }); let responseData; await act(async () => { responseData = await result.current.mutateAsync({ id: '1', context: { customer_name: 'John Doe', service: 'Consultation' }, }); }); expect(apiClient.post).toHaveBeenCalledWith('/contracts/templates/1/preview/', { customer_name: 'John Doe', service: 'Consultation', }); expect(responseData).toEqual(mockPreview); }); it('previews template without context', async () => { const mockPreview = { html: 'Default preview
' }; vi.mocked(apiClient.post).mockResolvedValue({ data: mockPreview }); const { result } = renderHook(() => usePreviewContractTemplate(), { wrapper: createWrapper(), }); await act(async () => { await result.current.mutateAsync({ id: '1' }); }); expect(apiClient.post).toHaveBeenCalledWith('/contracts/templates/1/preview/', {}); }); }); // --- Contracts --- describe('useContracts', () => { it('fetches all contracts and transforms data', async () => { const mockContracts = [ { id: 1, template: 10, template_name: 'Service Agreement', template_version: 1, scope: 'CUSTOMER', status: 'SIGNED', content_html: 'Contract content
', customer: 5, customer_name: 'John Doe', customer_email: 'john@example.com', appointment: 20, appointment_service_name: 'Consultation', appointment_start_time: '2024-01-15T10:00:00Z', service: 30, service_name: 'Consultation Service', sent_at: '2024-01-10T00:00:00Z', signed_at: '2024-01-11T00:00:00Z', expires_at: '2024-02-10T00:00:00Z', voided_at: null, voided_reason: null, signing_token: 'abc123', created_at: '2024-01-10T00:00:00Z', updated_at: '2024-01-11T00:00:00Z', }, { id: 2, template: 11, template_name: 'NDA', template_version: 2, scope: 'APPOINTMENT', status: 'PENDING', content: 'NDA content
', customer: null, customer_name: null, customer_email: null, appointment: null, appointment_service_name: null, appointment_start_time: null, service: null, service_name: null, sent_at: null, signed_at: null, expires_at: null, voided_at: null, voided_reason: null, public_token: 'xyz789', created_at: '2024-01-12T00:00:00Z', updated_at: '2024-01-12T00:00:00Z', }, ]; vi.mocked(apiClient.get).mockResolvedValue({ data: mockContracts }); const { result } = renderHook(() => useContracts(), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(apiClient.get).toHaveBeenCalledWith('/contracts/', { params: undefined }); expect(result.current.data).toHaveLength(2); expect(result.current.data?.[0]).toEqual({ id: '1', template: '10', template_name: 'Service Agreement', template_version: 1, scope: 'CUSTOMER', status: 'SIGNED', content: 'Contract content
', customer: '5', customer_name: 'John Doe', customer_email: 'john@example.com', appointment: '20', appointment_service_name: 'Consultation', appointment_start_time: '2024-01-15T10:00:00Z', service: '30', service_name: 'Consultation Service', sent_at: '2024-01-10T00:00:00Z', signed_at: '2024-01-11T00:00:00Z', expires_at: '2024-02-10T00:00:00Z', voided_at: null, voided_reason: null, public_token: 'abc123', created_at: '2024-01-10T00:00:00Z', updated_at: '2024-01-11T00:00:00Z', }); }); it('applies filters', async () => { vi.mocked(apiClient.get).mockResolvedValue({ data: [] }); renderHook( () => useContracts({ status: 'SIGNED', customer: '5', appointment: '20', }), { wrapper: createWrapper(), } ); await waitFor(() => { expect(apiClient.get).toHaveBeenCalledWith('/contracts/', { params: { status: 'SIGNED', customer: '5', appointment: '20', }, }); }); }); it('prefers content_html over content field', async () => { const mockContracts = [ { id: 1, template: 10, template_name: 'Agreement', template_version: 1, scope: 'CUSTOMER', status: 'PENDING', content: 'Fallback content
', content_html: 'Rendered HTML content
', signing_token: 'token123', created_at: '2024-01-10T00:00:00Z', updated_at: '2024-01-10T00:00:00Z', }, ]; vi.mocked(apiClient.get).mockResolvedValue({ data: mockContracts }); const { result } = renderHook(() => useContracts(), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(result.current.data?.[0].content).toBe('Rendered HTML content
'); }); it('uses signing_token as public_token', async () => { const mockContracts = [ { id: 1, template: 10, template_name: 'Agreement', template_version: 1, scope: 'CUSTOMER', status: 'PENDING', content: 'Content
', signing_token: 'secret-token', created_at: '2024-01-10T00:00:00Z', updated_at: '2024-01-10T00:00:00Z', }, ]; vi.mocked(apiClient.get).mockResolvedValue({ data: mockContracts }); const { result } = renderHook(() => useContracts(), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(result.current.data?.[0].public_token).toBe('secret-token'); }); }); describe('useContract', () => { it('fetches single contract by id', async () => { const mockContract = { id: 1, template: 10, template_name: 'Service Agreement', template_version: 1, scope: 'CUSTOMER', status: 'SIGNED', content: 'Contract content
', customer: 5, customer_name: 'John Doe', customer_email: 'john@example.com', appointment: 20, appointment_service_name: 'Consultation', appointment_start_time: '2024-01-15T10:00:00Z', service: 30, service_name: 'Consultation Service', sent_at: '2024-01-10T00:00:00Z', signed_at: '2024-01-11T00:00:00Z', expires_at: '2024-02-10T00:00:00Z', voided_at: null, voided_reason: null, signing_token: 'abc123', created_at: '2024-01-10T00:00:00Z', updated_at: '2024-01-11T00:00:00Z', }; vi.mocked(apiClient.get).mockResolvedValue({ data: mockContract }); const { result } = renderHook(() => useContract('1'), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(apiClient.get).toHaveBeenCalledWith('/contracts/1/'); expect(result.current.data).toEqual({ id: '1', template: '10', template_name: 'Service Agreement', template_version: 1, scope: 'CUSTOMER', status: 'SIGNED', content: 'Contract content
', customer: '5', customer_name: 'John Doe', customer_email: 'john@example.com', appointment: '20', appointment_service_name: 'Consultation', appointment_start_time: '2024-01-15T10:00:00Z', service: '30', service_name: 'Consultation Service', sent_at: '2024-01-10T00:00:00Z', signed_at: '2024-01-11T00:00:00Z', expires_at: '2024-02-10T00:00:00Z', voided_at: null, voided_reason: null, public_token: 'abc123', created_at: '2024-01-10T00:00:00Z', updated_at: '2024-01-11T00:00:00Z', }); }); it('does not fetch when id is empty', async () => { const { result } = renderHook(() => useContract(''), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isLoading).toBe(false); }); expect(apiClient.get).not.toHaveBeenCalled(); }); it('handles optional fields being null or undefined', async () => { const mockContract = { id: 2, template: 11, template_name: 'NDA', template_version: 1, scope: 'APPOINTMENT', status: 'PENDING', content: 'NDA content
', customer: null, customer_name: null, customer_email: null, appointment: null, appointment_service_name: null, appointment_start_time: null, service: null, service_name: null, sent_at: null, signed_at: null, expires_at: null, voided_at: null, voided_reason: null, public_token: 'token123', created_at: '2024-01-10T00:00:00Z', updated_at: '2024-01-10T00:00:00Z', }; vi.mocked(apiClient.get).mockResolvedValue({ data: mockContract }); const { result } = renderHook(() => useContract('2'), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(result.current.data?.customer).toBeUndefined(); expect(result.current.data?.customer_name).toBeUndefined(); expect(result.current.data?.appointment).toBeUndefined(); expect(result.current.data?.service).toBeUndefined(); }); }); describe('useCreateContract', () => { it('creates contract with all fields', async () => { vi.mocked(apiClient.post).mockResolvedValue({ data: { id: 1 } }); const { result } = renderHook(() => useCreateContract(), { wrapper: createWrapper(), }); await act(async () => { await result.current.mutateAsync({ template: '10', customer_id: '5', event_id: '20', send_email: true, }); }); expect(apiClient.post).toHaveBeenCalledWith('/contracts/', { template: '10', customer_id: '5', event_id: '20', send_email: true, }); }); it('creates contract with minimal fields', async () => { vi.mocked(apiClient.post).mockResolvedValue({ data: { id: 1 } }); const { result } = renderHook(() => useCreateContract(), { wrapper: createWrapper(), }); await act(async () => { await result.current.mutateAsync({ template: '10', }); }); expect(apiClient.post).toHaveBeenCalledWith('/contracts/', { template: '10', }); }); }); describe('useSendContract', () => { it('sends contract by id', async () => { const mockResponse = { status: 'sent', sent_at: '2024-01-10T00:00:00Z' }; vi.mocked(apiClient.post).mockResolvedValue({ data: mockResponse }); const { result } = renderHook(() => useSendContract(), { wrapper: createWrapper(), }); let responseData; await act(async () => { responseData = await result.current.mutateAsync('1'); }); expect(apiClient.post).toHaveBeenCalledWith('/contracts/1/send/'); expect(responseData).toEqual(mockResponse); }); }); describe('useVoidContract', () => { it('voids contract with reason', async () => { const mockResponse = { status: 'voided' }; vi.mocked(apiClient.post).mockResolvedValue({ data: mockResponse }); const { result } = renderHook(() => useVoidContract(), { wrapper: createWrapper(), }); let responseData; await act(async () => { responseData = await result.current.mutateAsync({ id: '1', reason: 'Customer requested cancellation', }); }); expect(apiClient.post).toHaveBeenCalledWith('/contracts/1/void/', { reason: 'Customer requested cancellation', }); expect(responseData).toEqual(mockResponse); }); }); describe('useResendContract', () => { it('resends contract by id', async () => { const mockResponse = { status: 'sent' }; vi.mocked(apiClient.post).mockResolvedValue({ data: mockResponse }); const { result } = renderHook(() => useResendContract(), { wrapper: createWrapper(), }); let responseData; await act(async () => { responseData = await result.current.mutateAsync('1'); }); expect(apiClient.post).toHaveBeenCalledWith('/contracts/1/resend/'); expect(responseData).toEqual(mockResponse); }); }); // --- Public Contract Access --- describe('usePublicContract', () => { it('fetches public contract view by token', async () => { const mockPublicView = { contract: { id: '1', content: 'Contract content
', status: 'PENDING', }, template: { name: 'Service Agreement', content: 'Template content
', }, business: { name: 'Acme Corp', logo_url: 'https://example.com/logo.png', }, customer: { name: 'John Doe', email: 'john@example.com', }, }; vi.mocked(apiClient.get).mockResolvedValue({ data: mockPublicView }); const { result } = renderHook(() => usePublicContract('abc123'), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(apiClient.get).toHaveBeenCalledWith('/contracts/sign/abc123/'); expect(result.current.data).toEqual(mockPublicView); }); it('does not fetch when token is empty', async () => { const { result } = renderHook(() => usePublicContract(''), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isLoading).toBe(false); }); expect(apiClient.get).not.toHaveBeenCalled(); }); }); describe('useSignContract', () => { it('signs contract with all required fields', async () => { const mockResponse = { status: 'signed', signed_at: '2024-01-10T00:00:00Z' }; vi.mocked(apiClient.post).mockResolvedValue({ data: mockResponse }); const { result } = renderHook(() => useSignContract(), { wrapper: createWrapper(), }); let responseData; await act(async () => { responseData = await result.current.mutateAsync({ token: 'abc123', signer_name: 'John Doe', consent_checkbox_checked: true, electronic_consent_given: true, }); }); expect(apiClient.post).toHaveBeenCalledWith('/contracts/sign/abc123/', { signer_name: 'John Doe', consent_checkbox_checked: true, electronic_consent_given: true, }); expect(responseData).toEqual(mockResponse); }); it('handles signing with consent checkboxes false', async () => { vi.mocked(apiClient.post).mockResolvedValue({ data: {} }); const { result } = renderHook(() => useSignContract(), { wrapper: createWrapper(), }); await act(async () => { await result.current.mutateAsync({ token: 'abc123', signer_name: 'Jane Smith', consent_checkbox_checked: false, electronic_consent_given: false, }); }); expect(apiClient.post).toHaveBeenCalledWith('/contracts/sign/abc123/', { signer_name: 'Jane Smith', consent_checkbox_checked: false, electronic_consent_given: false, }); }); }); describe('useExportLegalPackage', () => { it('calls API with correct parameters and triggers download', async () => { // Mock blob response const mockBlob = new Blob(['mock zip content'], { type: 'application/zip' }); const mockResponse = { data: mockBlob, headers: { 'content-disposition': 'attachment; filename="legal_export_contract_1.zip"', }, }; vi.mocked(apiClient.get).mockResolvedValue(mockResponse); // Mock DOM methods const mockClick = vi.fn(); const mockRemove = vi.fn(); const mockSetAttribute = vi.fn(); // Create a real anchor element and spy on it const mockLink = document.createElement('a'); vi.spyOn(mockLink, 'click').mockImplementation(mockClick); vi.spyOn(mockLink, 'remove').mockImplementation(mockRemove); vi.spyOn(mockLink, 'setAttribute').mockImplementation(mockSetAttribute); const mockCreateObjectURL = vi.fn().mockReturnValue('blob:mock-url'); const mockRevokeObjectURL = vi.fn(); // Store originals const origCreateObjectURL = global.URL.createObjectURL; const origRevokeObjectURL = global.URL.revokeObjectURL; // Setup mocks global.URL.createObjectURL = mockCreateObjectURL; global.URL.revokeObjectURL = mockRevokeObjectURL; // Spy on createElement but return mockLink only for 'a' tags const originalCreateElement = document.createElement.bind(document); const createElementSpy = vi.spyOn(document, 'createElement'); createElementSpy.mockImplementation((tagName: string) => { if (tagName === 'a') return mockLink; return originalCreateElement(tagName as any); }); const { result } = renderHook(() => useExportLegalPackage(), { wrapper: createWrapper(), }); let responseData; await act(async () => { responseData = await result.current.mutateAsync('1'); }); expect(apiClient.get).toHaveBeenCalledWith('/contracts/1/export_legal/', { responseType: 'blob', }); expect(mockCreateObjectURL).toHaveBeenCalledWith(expect.any(Blob)); expect(mockClick).toHaveBeenCalled(); expect(mockRemove).toHaveBeenCalled(); expect(mockRevokeObjectURL).toHaveBeenCalledWith('blob:mock-url'); expect(responseData).toEqual({ success: true }); // Cleanup global.URL.createObjectURL = origCreateObjectURL; global.URL.revokeObjectURL = origRevokeObjectURL; createElementSpy.mockRestore(); }); }); });