import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { MemoryRouter, Route, Routes } from 'react-router-dom'; import React from 'react'; // Mock react-i18next vi.mock('react-i18next', () => ({ useTranslation: () => ({ t: (key: string, options?: any) => { if (typeof options === 'object' && options.customerName) { return `Contract for ${options.customerName}`; } return key; }, }), })); // Mock hooks const mockUsePublicContract = vi.fn(); const mockUseSignContract = vi.fn(); vi.mock('../../hooks/useContracts', () => ({ usePublicContract: (token: string) => mockUsePublicContract(token), useSignContract: () => mockUseSignContract(), })); import ContractSigning from '../ContractSigning'; describe('ContractSigning', () => { const mockContractData = { contract: { id: '123', content: '

Contract Title

Contract terms and conditions...

', status: 'PENDING', }, template: { name: 'Service Agreement', }, business: { id: '1', name: 'Test Business', logo_url: 'https://example.com/logo.png', }, customer: { name: 'John Doe', email: 'john@example.com', }, can_sign: true, is_expired: false, }; const mockSignature = { signer_name: 'John Doe', signer_email: 'john@example.com', signed_at: '2024-01-15T10:30:00Z', }; beforeEach(() => { vi.clearAllMocks(); mockUsePublicContract.mockReturnValue({ data: mockContractData, isLoading: false, error: null, refetch: vi.fn(), }); mockUseSignContract.mockReturnValue({ mutateAsync: vi.fn().mockResolvedValue({}), isPending: false, isSuccess: false, isError: false, }); }); const renderComponent = (token = 'test-token') => { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false }, }, }); return render( } /> ); }; it('renders loading state', () => { mockUsePublicContract.mockReturnValue({ data: null, isLoading: true, error: null, refetch: vi.fn(), }); renderComponent(); expect(screen.getByText('common.loading')).toBeInTheDocument(); const loader = document.querySelector('.animate-spin'); expect(loader).toBeInTheDocument(); }); it('renders contract not found error', () => { mockUsePublicContract.mockReturnValue({ data: null, isLoading: false, error: new Error('Not found'), refetch: vi.fn(), }); renderComponent(); expect(screen.getByText('contracts.signing.notFound')).toBeInTheDocument(); expect(screen.getByText(/invalid or has expired/i)).toBeInTheDocument(); }); it('renders expired contract message', () => { mockUsePublicContract.mockReturnValue({ data: { ...mockContractData, is_expired: true }, isLoading: false, error: null, refetch: vi.fn(), }); renderComponent(); expect(screen.getByText('contracts.signing.expired')).toBeInTheDocument(); expect(screen.getByText(/can no longer be signed/i)).toBeInTheDocument(); }); it('renders contract signing form', () => { renderComponent(); expect(screen.getByText('Test Business')).toBeInTheDocument(); expect(screen.getByText('Service Agreement')).toBeInTheDocument(); expect(screen.getByText('Contract for John Doe')).toBeInTheDocument(); expect(screen.getByPlaceholderText('Enter your full name')).toBeInTheDocument(); }); it('renders contract content', () => { renderComponent(); const contractContent = document.querySelector('.prose'); expect(contractContent).toBeInTheDocument(); expect(contractContent?.innerHTML).toContain('Contract Title'); expect(contractContent?.innerHTML).toContain('Contract terms and conditions'); }); it('displays business logo when available', () => { renderComponent(); const logo = screen.getByAltText('Test Business'); expect(logo).toBeInTheDocument(); expect(logo).toHaveAttribute('src', 'https://example.com/logo.png'); }); it('shows signature input and preview', async () => { const user = userEvent.setup(); renderComponent(); const nameInput = screen.getByPlaceholderText('Enter your full name'); await user.type(nameInput, 'John Doe'); expect(nameInput).toHaveValue('John Doe'); // Check for signature preview await waitFor(() => { expect(screen.getByText('Signature Preview:')).toBeInTheDocument(); }); // Preview should show the typed name in cursive const preview = document.querySelector('[style*="cursive"]'); expect(preview).toBeInTheDocument(); expect(preview?.textContent).toBe('John Doe'); }); it('requires all consent checkboxes', async () => { const user = userEvent.setup(); renderComponent(); const nameInput = screen.getByPlaceholderText('Enter your full name'); await user.type(nameInput, 'John Doe'); const signButton = screen.getByRole('button', { name: /sign contract/i }); // Button should be disabled without consent expect(signButton).toBeDisabled(); // Check first checkbox const checkbox1 = screen.getByLabelText(/have read and agree/i); await user.click(checkbox1); // Still disabled without second checkbox expect(signButton).toBeDisabled(); // Check second checkbox const checkbox2 = screen.getByLabelText(/consent to conduct business electronically/i); await user.click(checkbox2); // Now should be enabled expect(signButton).toBeEnabled(); }); it('submits signature when all fields are valid', async () => { const user = userEvent.setup(); const mockMutate = vi.fn().mockResolvedValue({}); mockUseSignContract.mockReturnValue({ mutateAsync: mockMutate, isPending: false, isSuccess: false, isError: false, }); renderComponent(); // Fill name const nameInput = screen.getByPlaceholderText('Enter your full name'); await user.type(nameInput, 'John Doe'); // Check both checkboxes const checkbox1 = screen.getByLabelText(/have read and agree/i); const checkbox2 = screen.getByLabelText(/consent to conduct business electronically/i); await user.click(checkbox1); await user.click(checkbox2); // Submit const signButton = screen.getByRole('button', { name: /sign contract/i }); await user.click(signButton); await waitFor(() => { expect(mockMutate).toHaveBeenCalledWith({ token: 'test-token', signer_name: 'John Doe', consent_checkbox_checked: true, electronic_consent_given: true, }); }); }); it('shows loading state while signing', async () => { const user = userEvent.setup(); mockUseSignContract.mockReturnValue({ mutateAsync: vi.fn().mockImplementation(() => new Promise(() => {})), isPending: true, isSuccess: false, isError: false, }); renderComponent(); expect(screen.getByRole('button', { name: /signing/i })).toBeInTheDocument(); expect(screen.getByRole('button', { name: /signing/i })).toBeDisabled(); }); it('shows error message when signing fails', async () => { const user = userEvent.setup(); mockUseSignContract.mockReturnValue({ mutateAsync: vi.fn().mockRejectedValue(new Error('Signing failed')), isPending: false, isSuccess: false, isError: true, }); renderComponent(); await waitFor(() => { expect(screen.getByText(/failed to sign the contract/i)).toBeInTheDocument(); }); }); it('renders signed contract view after successful signing', () => { mockUsePublicContract.mockReturnValue({ data: { ...mockContractData, contract: { ...mockContractData.contract, status: 'SIGNED' }, signature: mockSignature, }, isLoading: false, error: null, refetch: vi.fn(), }); renderComponent(); expect(screen.getByText(/contract successfully signed/i)).toBeInTheDocument(); expect(screen.getByRole('button', { name: /print contract/i })).toBeInTheDocument(); }); it('displays signature details in signed view', () => { mockUsePublicContract.mockReturnValue({ data: { ...mockContractData, contract: { ...mockContractData.contract, status: 'SIGNED' }, signature: mockSignature, }, isLoading: false, error: null, refetch: vi.fn(), }); renderComponent(); // Use getAllByText since "John Doe" and "john@example.com" appear multiple times expect(screen.getAllByText('John Doe').length).toBeGreaterThan(0); expect(screen.getAllByText('john@example.com').length).toBeGreaterThan(0); // Check for "Signed" status badge const signedBadges = screen.queryAllByText(/^signed$/i); expect(signedBadges.length).toBeGreaterThan(0); }); it('handles print button click', async () => { const user = userEvent.setup(); const mockPrint = vi.fn(); window.print = mockPrint; mockUsePublicContract.mockReturnValue({ data: { ...mockContractData, contract: { ...mockContractData.contract, status: 'SIGNED' }, signature: mockSignature, }, isLoading: false, error: null, refetch: vi.fn(), }); renderComponent(); const printButton = screen.getByRole('button', { name: /print contract/i }); await user.click(printButton); expect(mockPrint).toHaveBeenCalled(); }); it('shows legal compliance notice in signing form', () => { renderComponent(); expect(screen.getByText(/ESIGN Act/i)).toBeInTheDocument(); expect(screen.getByText(/UETA/i)).toBeInTheDocument(); }); it('shows electronic consent disclosure', () => { renderComponent(); expect(screen.getByText(/conduct business electronically/i)).toBeInTheDocument(); expect(screen.getByText(/right to receive documents in paper form/i)).toBeInTheDocument(); }); it('prevents signing when cannot sign', () => { mockUsePublicContract.mockReturnValue({ data: { ...mockContractData, can_sign: false, }, isLoading: false, error: null, refetch: vi.fn(), }); renderComponent(); // Should not show the signing form expect(screen.queryByPlaceholderText('Enter your full name')).not.toBeInTheDocument(); }); it('validates name is not empty before enabling submit', async () => { const user = userEvent.setup(); renderComponent(); const checkbox1 = screen.getByLabelText(/have read and agree/i); const checkbox2 = screen.getByLabelText(/consent to conduct business electronically/i); await user.click(checkbox1); await user.click(checkbox2); const signButton = screen.getByRole('button', { name: /sign contract/i }); // Should be disabled with empty name expect(signButton).toBeDisabled(); // Type name const nameInput = screen.getByPlaceholderText('Enter your full name'); await user.type(nameInput, 'John Doe'); // Should be enabled now expect(signButton).toBeEnabled(); }); });