Files
smoothschedule/frontend/src/hooks/__tests__/useContracts.test.ts
poduck 8dc2248f1f feat: Add comprehensive test suite and misc improvements
- Add frontend unit tests with Vitest for components, hooks, pages, and utilities
- Add backend tests for webhooks, notifications, middleware, and edge cases
- Add ForgotPassword, NotFound, and ResetPassword pages
- Add migration for orphaned staff resources conversion
- Add coverage directory to gitignore (generated reports)
- Various bug fixes and improvements from previous work

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-08 02:36:46 -05:00

1008 lines
30 KiB
TypeScript

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: '<p>Service terms...</p>',
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: '<p>Confidentiality terms...</p>',
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: '<p>Service terms...</p>',
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: '<p>Confidentiality terms...</p>',
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: '<p>Service terms...</p>',
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: '<p>Service terms...</p>',
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: '<p>NDA content</p>',
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: '<p>Template content</p>',
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: '<p>Template content</p>',
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: '<p>Content</p>',
scope: 'APPOINTMENT',
});
});
expect(apiClient.post).toHaveBeenCalledWith('/contracts/templates/', {
name: 'Minimal Template',
content: '<p>Content</p>',
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: '<p>Preview with John Doe</p>' };
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: '<p>Default preview</p>' };
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: '<p>Contract content</p>',
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: '<p>NDA content</p>',
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: '<p>Contract content</p>',
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: '<p>Fallback content</p>',
content_html: '<p>Rendered HTML content</p>',
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('<p>Rendered HTML content</p>');
});
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: '<p>Content</p>',
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: '<p>Contract content</p>',
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: '<p>Contract content</p>',
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: '<p>NDA content</p>',
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: '<p>Contract content</p>',
status: 'PENDING',
},
template: {
name: 'Service Agreement',
content: '<p>Template content</p>',
},
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();
});
});
});