/** * @vitest-environment jsdom */ import React from 'react'; import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; import { act, cleanup, render, screen, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import '@testing-library/jest-dom/vitest'; let mockDraftContracts: any[] = []; let mockDraftResumeData: any = {}; const mockRouter = { push: vi.fn(), replace: vi.fn(), }; vi.mock('next/navigation', () => ({ useRouter: () => mockRouter, useSearchParams: () => new URLSearchParams('tab=contracts&subtab=drafts'), })); vi.mock('react-hot-toast', () => ({ toast: { success: vi.fn(), error: vi.fn(), }, })); // Interpolating t() mock so messages like 'delete the draft "{{contractName}}"' // render with their values instead of raw placeholders. vi.mock('@alga-psa/ui/lib/i18n/client', () => ({ useTranslation: () => ({ t: (key: string, opts?: string | (Record & { defaultValue?: string })) => { if (typeof opts === 'string') { return opts; } let result = typeof opts?.defaultValue === 'string' ? opts.defaultValue : key; if (opts) { for (const [name, value] of Object.entries(opts)) { if (name === 'defaultValue') continue; result = result.split(`{{${name}}}`).join(String(value)); } } return result; }, }), useFormatters: () => ({ formatDate: (value: unknown) => String(value), formatCurrency: (value: number) => `$${value}`, }), })); vi.mock('@alga-psa/ui/components/CustomTabs', () => ({ default: ({ tabs, defaultTab, }: { tabs: Array<{ id?: string; label: string; content: React.ReactNode }>; defaultTab: string; }) => { const tab = tabs.find((t) => t.id === defaultTab) ?? tabs.find((t) => t.label === defaultTab) ?? tabs[0]; return (
{tabs.map((t) => ( {t.label} {(t as { icon?: React.ReactNode }).icon ?? null} ))}
{tab?.content}
); }, })); vi.mock('@alga-psa/ui/components/ConfirmationDialog', () => ({ ConfirmationDialog: ({ isOpen, title, message, cancelLabel, confirmLabel, onClose, onConfirm, isConfirming, }: { isOpen: boolean; title: string; message: string; cancelLabel: string; confirmLabel: string; onClose: () => void; onConfirm: () => void; isConfirming?: boolean; }) => isOpen ? (
{title}
{message}
) : null, })); vi.mock('../src/components/billing-dashboard/contracts/ContractWizard', () => ({ ContractWizard: ({ open, editingContract }: { open: boolean; editingContract?: any }) => open ? (
) : null, })); vi.mock('../src/components/billing-dashboard/contracts/template-wizard/TemplateWizard', () => ({ TemplateWizard: () => null, })); vi.mock('../src/components/billing-dashboard/contracts/ContractDialog', () => ({ ContractDialog: () => null, })); vi.mock('@alga-psa/billing/actions/contractActions', () => ({ deleteContract: vi.fn(async () => undefined), getContractTemplates: vi.fn(async () => []), getContractsWithClients: vi.fn(async () => []), getDraftContracts: vi.fn(async () => mockDraftContracts), updateContract: vi.fn(async () => undefined), })); vi.mock('@alga-psa/billing/actions/contractWizardActions', () => ({ getDraftContractForResume: vi.fn(async () => mockDraftResumeData), })); describe('Drafts tab DataTable', () => { beforeAll(() => { (globalThis as any).IS_REACT_ACT_ENVIRONMENT = true; Object.defineProperty(HTMLElement.prototype, 'clientWidth', { configurable: true, get() { return 1200; }, }); }); beforeEach(async () => { document.body.removeAttribute('data-scroll-locked'); document.body.removeAttribute('style'); cleanup(); mockDraftContracts = []; mockDraftResumeData = {}; vi.clearAllMocks(); const contractActions = await import('@alga-psa/billing/actions/contractActions'); vi.mocked(contractActions.deleteContract).mockReset(); vi.mocked(contractActions.deleteContract).mockResolvedValue(undefined); vi.mocked(contractActions.getContractTemplates).mockReset(); vi.mocked(contractActions.getContractTemplates).mockResolvedValue([]); vi.mocked(contractActions.getContractsWithClients).mockReset(); vi.mocked(contractActions.getContractsWithClients).mockResolvedValue([]); vi.mocked(contractActions.getDraftContracts).mockReset(); vi.mocked(contractActions.getDraftContracts).mockImplementation(async () => mockDraftContracts); vi.mocked(contractActions.updateContract).mockReset(); vi.mocked(contractActions.updateContract).mockResolvedValue(undefined); const wizardActions = await import('@alga-psa/billing/actions/contractWizardActions'); vi.mocked(wizardActions.getDraftContractForResume).mockReset(); vi.mocked(wizardActions.getDraftContractForResume).mockImplementation(async () => mockDraftResumeData); }); it('renders contract name for each draft (T012)', async () => { mockDraftContracts = [ { contract_id: 'contract-1', contract_name: 'Draft Alpha', client_name: 'Acme Co', created_at: '2026-01-01T00:00:00.000Z', updated_at: '2026-01-02T00:00:00.000Z', }, ]; const Contracts = (await import('../src/components/billing-dashboard/contracts/Contracts')).default; render(); expect(await screen.findByText('Draft Alpha')).toBeInTheDocument(); }); it('renders client name for each draft (T013)', async () => { mockDraftContracts = [ { contract_id: 'contract-1', contract_name: 'Draft Alpha', client_name: 'Acme Co', created_at: '2026-01-01T00:00:00.000Z', updated_at: '2026-01-02T00:00:00.000Z', }, ]; const Contracts = (await import('../src/components/billing-dashboard/contracts/Contracts')).default; render(); expect(await screen.findByText('Acme Co')).toBeInTheDocument(); }); it('renders created date in localized format (T014)', async () => { const createdAt = new Date(2026, 0, 1); const updatedAt = new Date(2026, 0, 2); mockDraftContracts = [ { contract_id: 'contract-1', contract_name: 'Draft Alpha', client_name: 'Acme Co', created_at: createdAt, updated_at: updatedAt, }, ]; const Contracts = (await import('../src/components/billing-dashboard/contracts/Contracts')).default; render(); expect(await screen.findByText(createdAt.toLocaleDateString())).toBeInTheDocument(); }); it('renders last modified date in localized format (T015)', async () => { const createdAt = new Date(2026, 0, 1); const updatedAt = new Date(2026, 0, 5); mockDraftContracts = [ { contract_id: 'contract-1', contract_name: 'Draft Alpha', client_name: 'Acme Co', created_at: createdAt, updated_at: updatedAt, }, ]; const Contracts = (await import('../src/components/billing-dashboard/contracts/Contracts')).default; render(); expect(await screen.findByText(updatedAt.toLocaleDateString())).toBeInTheDocument(); }); it('renders actions dropdown for each row (T016)', async () => { mockDraftContracts = [ { contract_id: 'contract-1', contract_name: 'Draft Alpha', client_name: 'Acme Co', created_at: new Date(2026, 0, 1), updated_at: new Date(2026, 0, 2), }, { contract_id: 'contract-2', contract_name: 'Draft Beta', client_name: 'Beta LLC', created_at: new Date(2026, 0, 1), updated_at: new Date(2026, 0, 3), }, ]; const Contracts = (await import('../src/components/billing-dashboard/contracts/Contracts')).default; render(); const actionsButtons = await screen.findAllByRole('button', { name: /open menu/i }); expect(actionsButtons).toHaveLength(2); }); it('actions dropdown contains Resume option (T017)', async () => { mockDraftContracts = [ { contract_id: 'contract-1', contract_name: 'Draft Alpha', client_name: 'Acme Co', created_at: new Date(2026, 0, 1), updated_at: new Date(2026, 0, 2), }, ]; const Contracts = (await import('../src/components/billing-dashboard/contracts/Contracts')).default; render(); const user = userEvent.setup(); const actionsButton = await screen.findByRole('button', { name: /open menu/i }); await act(async () => { await user.click(actionsButton); }); expect(await screen.findByText('Resume')).toBeInTheDocument(); }); it('actions dropdown contains Discard option (T018)', async () => { mockDraftContracts = [ { contract_id: 'contract-1', contract_name: 'Draft Alpha', client_name: 'Acme Co', created_at: new Date(2026, 0, 1), updated_at: new Date(2026, 0, 2), }, ]; const Contracts = (await import('../src/components/billing-dashboard/contracts/Contracts')).default; render(); const user = userEvent.setup(); const actionsButton = await screen.findByRole('button', { name: /open menu/i }); await act(async () => { await user.click(actionsButton); }); expect(await screen.findByText('Discard')).toBeInTheDocument(); }); it('clicking column header sorts by that column (T019)', async () => { mockDraftContracts = [ { contract_id: 'contract-1', contract_name: 'b draft', client_name: 'Acme Co', created_at: new Date(2026, 0, 1), updated_at: new Date(2026, 0, 10), }, { contract_id: 'contract-2', contract_name: 'A draft', client_name: 'Beta LLC', created_at: new Date(2026, 0, 1), updated_at: new Date(2026, 0, 5), }, ]; const Contracts = (await import('../src/components/billing-dashboard/contracts/Contracts')).default; render(); // Initial sorting is by updated_at desc, so "b draft" should be first. await waitFor(async () => { const rows = await screen.findAllByRole('row'); const firstDataRow = rows[1]; expect(within(firstDataRow).getByText('b draft')).toBeInTheDocument(); }); const user = userEvent.setup(); const contractNameHeader = screen.getByRole('columnheader', { name: /contract name/i }); await act(async () => { await user.click(contractNameHeader); }); await waitFor(() => { const rows = screen.getAllByRole('row'); const firstDataRow = rows[1]; expect(within(firstDataRow).getByText('A draft')).toBeInTheDocument(); }); }); it('search input filters drafts by contract name (T020)', async () => { mockDraftContracts = [ { contract_id: 'contract-1', contract_name: 'Draft Alpha', client_name: 'Acme Co', created_at: new Date(2026, 0, 1), updated_at: new Date(2026, 0, 2), }, { contract_id: 'contract-2', contract_name: 'Draft Beta', client_name: 'Beta LLC', created_at: new Date(2026, 0, 1), updated_at: new Date(2026, 0, 3), }, ]; const Contracts = (await import('../src/components/billing-dashboard/contracts/Contracts')).default; render(); await screen.findByText('Draft Alpha'); await screen.findByText('Draft Beta'); const user = userEvent.setup(); const searchInput = screen.getByLabelText('Search draft contracts'); await act(async () => { await user.type(searchInput, 'Alpha'); }); await waitFor(() => { expect(screen.getByText('Draft Alpha')).toBeInTheDocument(); expect(screen.queryByText('Draft Beta')).not.toBeInTheDocument(); }); }); it('search input filters drafts by client name (T021)', async () => { mockDraftContracts = [ { contract_id: 'contract-1', contract_name: 'Draft Alpha', client_name: 'Acme Co', created_at: new Date(2026, 0, 1), updated_at: new Date(2026, 0, 2), }, { contract_id: 'contract-2', contract_name: 'Draft Beta', client_name: 'Beta LLC', created_at: new Date(2026, 0, 1), updated_at: new Date(2026, 0, 3), }, ]; const Contracts = (await import('../src/components/billing-dashboard/contracts/Contracts')).default; render(); await screen.findByText('Draft Alpha'); await screen.findByText('Draft Beta'); const user = userEvent.setup(); const searchInput = screen.getByLabelText('Search draft contracts'); await act(async () => { await user.type(searchInput, 'Beta'); }); await waitFor(() => { expect(screen.getByText('Draft Beta')).toBeInTheDocument(); expect(screen.queryByText('Draft Alpha')).not.toBeInTheDocument(); }); }); it('search is case-insensitive (T022)', async () => { mockDraftContracts = [ { contract_id: 'contract-1', contract_name: 'Draft Alpha', client_name: 'Acme Co', created_at: new Date(2026, 0, 1), updated_at: new Date(2026, 0, 2), }, ]; const Contracts = (await import('../src/components/billing-dashboard/contracts/Contracts')).default; render(); await screen.findByText('Draft Alpha'); const user = userEvent.setup(); const searchInput = screen.getByLabelText('Search draft contracts'); await act(async () => { await user.type(searchInput, 'acme'); }); expect(await screen.findByText('Draft Alpha')).toBeInTheDocument(); }); it('pagination controls appear when drafts exceed page size (T023)', async () => { mockDraftContracts = Array.from({ length: 11 }, (_v, idx) => ({ contract_id: `contract-${idx + 1}`, contract_name: `Draft ${idx + 1}`, client_name: `Client ${idx + 1}`, created_at: new Date(2026, 0, 1), updated_at: new Date(2026, 0, idx + 1), })); const Contracts = (await import('../src/components/billing-dashboard/contracts/Contracts')).default; render(); expect(await screen.findByText('Draft 11')).toBeInTheDocument(); expect(screen.getByLabelText('Pagination')).toBeInTheDocument(); expect(screen.getByRole('button', { name: '2' })).toBeInTheDocument(); const nextButton = document.getElementById('draft-contracts-table-pagination-next-btn') as HTMLButtonElement | null; expect(nextButton).not.toBeNull(); expect(nextButton?.disabled).toBe(false); }); it('pagination controls navigate between pages (T024)', async () => { mockDraftContracts = Array.from({ length: 11 }, (_v, idx) => ({ contract_id: `contract-${idx + 1}`, contract_name: `Draft ${idx + 1}`, client_name: `Client ${idx + 1}`, created_at: new Date(2026, 0, 1), updated_at: new Date(2026, 0, idx + 1), })); const Contracts = (await import('../src/components/billing-dashboard/contracts/Contracts')).default; render(); expect(await screen.findByText('Draft 11')).toBeInTheDocument(); expect(screen.queryByText('Draft 1')).not.toBeInTheDocument(); const user = userEvent.setup(); const pageTwoButton = screen.getByRole('button', { name: '2' }); await act(async () => { await user.click(pageTwoButton); }); await waitFor(() => { expect(screen.getByText('Draft 1')).toBeInTheDocument(); expect(screen.queryByText('Draft 11')).not.toBeInTheDocument(); }); }); it('empty state displays when no drafts exist (T025)', async () => { mockDraftContracts = []; const Contracts = (await import('../src/components/billing-dashboard/contracts/Contracts')).default; render(); expect( await screen.findByText('No draft contracts. Start creating a new contract to save as draft.'), ).toBeInTheDocument(); }); it('empty state message mentions saving drafts (T026)', async () => { mockDraftContracts = []; const Contracts = (await import('../src/components/billing-dashboard/contracts/Contracts')).default; render(); expect(await screen.findByText(/start creating a new contract to save as draft/i)).toBeInTheDocument(); }); it('clicking Resume opens ContractWizard dialog (T031)', async () => { mockDraftResumeData = { contract_id: 'contract-1', is_draft: true, client_id: 'client-1', contract_name: 'Draft Alpha', start_date: '2026-01-01', currency_code: 'USD', enable_proration: false, fixed_services: [], product_services: [], hourly_services: [], usage_services: [], }; mockDraftContracts = [ { contract_id: 'contract-1', contract_name: 'Draft Alpha', client_name: 'Acme Co', created_at: new Date(2026, 0, 1), updated_at: new Date(2026, 0, 2), }, ]; const Contracts = (await import('../src/components/billing-dashboard/contracts/Contracts')).default; render(); const user = userEvent.setup(); const actionsButton = await screen.findByRole('button', { name: /open menu/i }); await act(async () => { await user.click(actionsButton); }); const resumeItem = await screen.findByText('Resume'); await act(async () => { await user.click(resumeItem); }); await waitFor(() => { const wizard = screen.getByTestId('contract-wizard'); expect(wizard).toHaveAttribute('data-contract-id', 'contract-1'); }); }); it('resumed wizard displays with draft data loaded (T032)', async () => { mockDraftResumeData = { contract_id: 'contract-1', is_draft: true, client_id: 'client-1', contract_name: 'Draft Alpha', start_date: '2026-01-01', currency_code: 'USD', enable_proration: false, fixed_services: [], product_services: [], hourly_services: [], usage_services: [], }; mockDraftContracts = [ { contract_id: 'contract-1', contract_name: 'Draft Alpha', client_name: 'Acme Co', created_at: new Date(2026, 0, 1), updated_at: new Date(2026, 0, 2), }, ]; const Contracts = (await import('../src/components/billing-dashboard/contracts/Contracts')).default; render(); const user = userEvent.setup(); const actionsButton = await screen.findByRole('button', { name: /open menu/i }); await act(async () => { await user.click(actionsButton); }); const resumeItem = await screen.findByText('Resume'); await act(async () => { await user.click(resumeItem); }); await waitFor(() => { const wizard = screen.getByTestId('contract-wizard'); expect(wizard).toHaveAttribute('data-contract-id', 'contract-1'); expect(wizard).toHaveAttribute('data-client-id', 'client-1'); expect(wizard).toHaveAttribute('data-contract-name', 'Draft Alpha'); }); }); it('clicking Discard opens confirmation dialog (T049)', async () => { mockDraftContracts = [ { contract_id: 'contract-1', contract_name: 'Draft Alpha', client_name: 'Acme Co', created_at: new Date(2026, 0, 1), updated_at: new Date(2026, 0, 2), }, ]; const Contracts = (await import('../src/components/billing-dashboard/contracts/Contracts')).default; render(); const user = userEvent.setup(); const actionsButton = await screen.findByRole('button', { name: /open menu/i }); await act(async () => { await user.click(actionsButton); }); await act(async () => { await user.click(await screen.findByText('Discard')); }); expect(await screen.findByTestId('confirmation-dialog')).toBeInTheDocument(); }); it('confirmation dialog displays contract name (T050)', async () => { mockDraftContracts = [ { contract_id: 'contract-1', contract_name: 'Draft Alpha', client_name: 'Acme Co', created_at: new Date(2026, 0, 1), updated_at: new Date(2026, 0, 2), }, ]; const Contracts = (await import('../src/components/billing-dashboard/contracts/Contracts')).default; render(); await screen.findByText('Draft Alpha'); const user = userEvent.setup(); await act(async () => { await user.click(await screen.findByRole('button', { name: /open menu/i })); }); await act(async () => { await user.click(await screen.findByText('Discard')); }); const dialog = await screen.findByTestId('confirmation-dialog'); expect(within(dialog).getByText(/Draft Alpha/)).toBeInTheDocument(); }); it('confirmation dialog displays client name (T051)', async () => { mockDraftContracts = [ { contract_id: 'contract-1', contract_name: 'Draft Alpha', client_name: 'Acme Co', created_at: new Date(2026, 0, 1), updated_at: new Date(2026, 0, 2), }, ]; const Contracts = (await import('../src/components/billing-dashboard/contracts/Contracts')).default; render(); await screen.findByText('Draft Alpha'); const user = userEvent.setup(); await act(async () => { await user.click(await screen.findByRole('button', { name: /open menu/i })); }); await act(async () => { await user.click(await screen.findByText('Discard')); }); const dialog = await screen.findByTestId('confirmation-dialog'); expect(within(dialog).getByText(/Acme Co/)).toBeInTheDocument(); }); it('confirmation dialog displays warning about permanent deletion (T052)', async () => { mockDraftContracts = [ { contract_id: 'contract-1', contract_name: 'Draft Alpha', client_name: 'Acme Co', created_at: new Date(2026, 0, 1), updated_at: new Date(2026, 0, 2), }, ]; const Contracts = (await import('../src/components/billing-dashboard/contracts/Contracts')).default; render(); await screen.findByText('Draft Alpha'); const user = userEvent.setup(); await act(async () => { await user.click(await screen.findByRole('button', { name: /open menu/i })); }); await act(async () => { await user.click(await screen.findByText('Discard')); }); const dialog = await screen.findByTestId('confirmation-dialog'); expect(within(dialog).getByText(/permanently delete/i)).toBeInTheDocument(); expect(within(dialog).getByText(/cannot be undone/i)).toBeInTheDocument(); }); it('confirmation dialog has Cancel button (T053)', async () => { mockDraftContracts = [ { contract_id: 'contract-1', contract_name: 'Draft Alpha', client_name: 'Acme Co', created_at: new Date(2026, 0, 1), updated_at: new Date(2026, 0, 2), }, ]; const Contracts = (await import('../src/components/billing-dashboard/contracts/Contracts')).default; render(); await screen.findByText('Draft Alpha'); const user = userEvent.setup(); await act(async () => { await user.click(await screen.findByRole('button', { name: /open menu/i })); }); await act(async () => { await user.click(await screen.findByText('Discard')); }); const dialog = await screen.findByTestId('confirmation-dialog'); expect(within(dialog).getByRole('button', { name: 'Cancel' })).toBeInTheDocument(); }); it('confirmation dialog has Discard button (T054)', async () => { mockDraftContracts = [ { contract_id: 'contract-1', contract_name: 'Draft Alpha', client_name: 'Acme Co', created_at: new Date(2026, 0, 1), updated_at: new Date(2026, 0, 2), }, ]; const Contracts = (await import('../src/components/billing-dashboard/contracts/Contracts')).default; render(); await screen.findByText('Draft Alpha'); const user = userEvent.setup(); await act(async () => { await user.click(await screen.findByRole('button', { name: /open menu/i })); }); await act(async () => { await user.click(await screen.findByText('Discard')); }); const dialog = await screen.findByTestId('confirmation-dialog'); expect(within(dialog).getByRole('button', { name: 'Discard' })).toBeInTheDocument(); }); it('clicking Cancel closes dialog without deleting (T055)', async () => { mockDraftContracts = [ { contract_id: 'contract-1', contract_name: 'Draft Alpha', client_name: 'Acme Co', created_at: new Date(2026, 0, 1), updated_at: new Date(2026, 0, 2), }, ]; const { deleteContract } = await import('@alga-psa/billing/actions/contractActions'); const Contracts = (await import('../src/components/billing-dashboard/contracts/Contracts')).default; render(); await screen.findByText('Draft Alpha'); const user = userEvent.setup(); await act(async () => { await user.click(await screen.findByRole('button', { name: /open menu/i })); }); await act(async () => { await user.click(await screen.findByText('Discard')); }); expect(await screen.findByTestId('confirmation-dialog')).toBeInTheDocument(); await act(async () => { await user.click(screen.getByRole('button', { name: 'Cancel' })); }); await waitFor(() => { expect(screen.queryByTestId('confirmation-dialog')).not.toBeInTheDocument(); }); expect(deleteContract).not.toHaveBeenCalled(); }); it('clicking Discard calls deleteContract action (T056)', async () => { mockDraftContracts = [ { contract_id: 'contract-1', contract_name: 'Draft Alpha', client_name: 'Acme Co', created_at: new Date(2026, 0, 1), updated_at: new Date(2026, 0, 2), }, ]; const { deleteContract } = await import('@alga-psa/billing/actions/contractActions'); const Contracts = (await import('../src/components/billing-dashboard/contracts/Contracts')).default; render(); await screen.findByText('Draft Alpha'); const user = userEvent.setup(); await act(async () => { await user.click(await screen.findByRole('button', { name: /open menu/i })); }); await act(async () => { await user.click(await screen.findByText('Discard')); }); await act(async () => { await user.click(screen.getByRole('button', { name: 'Discard' })); }); await waitFor(() => { expect(deleteContract).toHaveBeenCalledWith('contract-1'); }); }); it('deleted draft no longer appears in list (T057)', async () => { mockDraftContracts = [ { contract_id: 'contract-1', contract_name: 'Draft Alpha', client_name: 'Acme Co', created_at: new Date(2026, 0, 1), updated_at: new Date(2026, 0, 2), }, ]; const { deleteContract } = await import('@alga-psa/billing/actions/contractActions'); (deleteContract as unknown as { mockImplementationOnce: (fn: any) => void }).mockImplementationOnce(async () => { mockDraftContracts = []; }); const Contracts = (await import('../src/components/billing-dashboard/contracts/Contracts')).default; render(); await screen.findByText('Draft Alpha'); const user = userEvent.setup(); await act(async () => { await user.click(await screen.findByRole('button', { name: /open menu/i })); }); await act(async () => { await user.click(await screen.findByText('Discard')); }); await act(async () => { await user.click(screen.getByRole('button', { name: 'Discard' })); }); await waitFor(() => { expect(screen.queryByText('Draft Alpha')).not.toBeInTheDocument(); }); expect( await screen.findByText('No draft contracts. Start creating a new contract to save as draft.'), ).toBeInTheDocument(); }); it('deletion shows success toast notification (T058)', async () => { mockDraftContracts = [ { contract_id: 'contract-1', contract_name: 'Draft Alpha', client_name: 'Acme Co', created_at: new Date(2026, 0, 1), updated_at: new Date(2026, 0, 2), }, ]; const { toast } = await import('react-hot-toast'); const { deleteContract } = await import('@alga-psa/billing/actions/contractActions'); (deleteContract as unknown as { mockResolvedValueOnce: (val: any) => void }).mockResolvedValueOnce(undefined); const Contracts = (await import('../src/components/billing-dashboard/contracts/Contracts')).default; render(); await screen.findByText('Draft Alpha'); const user = userEvent.setup(); await act(async () => { await user.click(await screen.findByRole('button', { name: /open menu/i })); }); await act(async () => { await user.click(await screen.findByText('Discard')); }); await act(async () => { await user.click(screen.getByRole('button', { name: 'Discard' })); }); await waitFor(() => { expect(toast.success).toHaveBeenCalledWith('Draft discarded'); }); }); it('deletion error shows error toast notification (T059)', async () => { mockDraftContracts = [ { contract_id: 'contract-1', contract_name: 'Draft Alpha', client_name: 'Acme Co', created_at: new Date(2026, 0, 1), updated_at: new Date(2026, 0, 2), }, ]; const { toast } = await import('react-hot-toast'); const { deleteContract } = await import('@alga-psa/billing/actions/contractActions'); (deleteContract as unknown as { mockRejectedValueOnce: (val: any) => void }).mockRejectedValueOnce( new Error('Delete failed'), ); const Contracts = (await import('../src/components/billing-dashboard/contracts/Contracts')).default; render(); await screen.findByText('Draft Alpha'); const user = userEvent.setup(); await act(async () => { await user.click(await screen.findByRole('button', { name: /open menu/i })); }); await act(async () => { await user.click(await screen.findByText('Discard')); }); await act(async () => { await user.click(screen.getByRole('button', { name: 'Discard' })); }); await waitFor(() => { // Discard failures route through handleError, which prefers the provided // fallback message over the raw error message. expect(toast.error).toHaveBeenCalledWith('Failed to discard draft'); }); expect(toast.success).not.toHaveBeenCalled(); }); it('drafts list refreshes automatically after successful deletion (T060)', async () => { mockDraftContracts = [ { contract_id: 'contract-1', contract_name: 'Draft Alpha', client_name: 'Acme Co', created_at: new Date(2026, 0, 1), updated_at: new Date(2026, 0, 2), }, ]; const { deleteContract, getDraftContracts } = await import('@alga-psa/billing/actions/contractActions'); (deleteContract as unknown as { mockImplementationOnce: (fn: any) => void }).mockImplementationOnce(async () => { mockDraftContracts = []; }); const Contracts = (await import('../src/components/billing-dashboard/contracts/Contracts')).default; render(); await screen.findByText('Draft Alpha'); expect(getDraftContracts).toHaveBeenCalledTimes(1); const user = userEvent.setup(); await act(async () => { await user.click(await screen.findByRole('button', { name: /open menu/i })); }); await act(async () => { await user.click(await screen.findByText('Discard')); }); await act(async () => { await user.click(screen.getByRole('button', { name: 'Discard' })); }); await waitFor(() => { expect(getDraftContracts).toHaveBeenCalledTimes(2); }); }); it('tab badge count updates after deletion (T061)', async () => { mockDraftContracts = [ { contract_id: 'contract-1', contract_name: 'Draft Alpha', client_name: 'Acme Co', created_at: new Date(2026, 0, 1), updated_at: new Date(2026, 0, 2), }, ]; const { deleteContract } = await import('@alga-psa/billing/actions/contractActions'); (deleteContract as unknown as { mockImplementationOnce: (fn: any) => void }).mockImplementationOnce(async () => { mockDraftContracts = []; }); const Contracts = (await import('../src/components/billing-dashboard/contracts/Contracts')).default; render(); await screen.findByText('Draft Alpha'); const badgeContainer = screen.getByTestId('tab-icon-Drafts'); expect(within(badgeContainer).getByText('1')).toBeInTheDocument(); const user = userEvent.setup(); await act(async () => { await user.click(await screen.findByRole('button', { name: /open menu/i })); }); await act(async () => { await user.click(await screen.findByText('Discard')); }); await act(async () => { await user.click(screen.getByRole('button', { name: 'Discard' })); }); await waitFor(() => { expect(within(badgeContainer).queryByText('1')).not.toBeInTheDocument(); }); }); });