/** * @vitest-environment jsdom */ import React from 'react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import '@testing-library/jest-dom/vitest'; let mockDueWorkResponse: any; let mockRecurringInvoiceHistoryResponse: any; const mockPreviewGroupedInvoicesForSelectionInputs = vi.fn(async (groups: Array<{ previewGroupKey: string; selectorInputs: any[] }>) => ({ success: true, invoiceCount: groups.length, previews: groups.map((group) => ({ previewGroupKey: group.previewGroupKey, selectorInputs: group.selectorInputs, data: { invoiceNumber: 'PREVIEW', issueDate: '2026-03-01', dueDate: '2026-03-31', customer: { name: 'Acme Co', address: '123 Main St' }, items: [], subtotal: 0, tax: 0, total: 0, }, })), })); const mockGenerateGroupedInvoicesAsRecurringBillingRun = vi.fn(async () => ({ failures: [] })); vi.mock('next/navigation', () => ({ useRouter: () => ({ push: vi.fn(), replace: vi.fn(), }), })); vi.mock('@alga-psa/ui/lib/i18n/client', () => ({ useTranslation: () => ({ t: (key: string, opts?: { defaultValue?: string } & Record) => (opts && typeof opts.defaultValue === 'string' ? opts.defaultValue : key), }), useFormatters: () => ({ formatDate: (value: unknown) => String(value), formatCurrency: (value: number) => `$${value}`, }), })); vi.mock('@alga-psa/billing/actions/billingAndTax', () => ({ getAvailableRecurringDueWork: vi.fn(async () => mockDueWorkResponse), })); vi.mock('@alga-psa/billing/actions/invoiceGeneration', () => ({ getPurchaseOrderOverageForSelectionInput: vi.fn(async () => ({ overage_cents: 0, po_number: null })), previewGroupedInvoicesForSelectionInputs: mockPreviewGroupedInvoicesForSelectionInputs, })); vi.mock('@alga-psa/billing/actions/recurringBillingRunActions', () => ({ generateInvoicesAsRecurringBillingRun: vi.fn(async () => ({ failures: [] })), generateGroupedInvoicesAsRecurringBillingRun: mockGenerateGroupedInvoicesAsRecurringBillingRun, })); vi.mock('@alga-psa/billing/actions/billingCycleActions', () => ({ getRecurringInvoiceHistoryPaginated: vi.fn(async () => mockRecurringInvoiceHistoryResponse), reverseRecurringInvoice: vi.fn(async () => undefined), hardDeleteRecurringInvoice: vi.fn(async () => undefined), })); vi.mock('@alga-psa/ui/components/DataTable', () => ({ DataTable: ({ id, data, columns = [], }: { id: string; data: any[]; columns?: Array<{ dataIndex?: string; render?: (value: unknown, row: any, index: number) => React.ReactNode }>; }) => (
{columns.map((column, columnIndex) => (
{(column as any).title ?? null}
))}
{data.length}
{data.map((row, index) => (
{columns.map((column, columnIndex) => { const value = column.dataIndex ? row[column.dataIndex] : undefined; return (
{column.render ? column.render(value, row, index) : String(value ?? '')}
); })}
))}
), })); vi.mock('@alga-psa/ui/components/Button', () => ({ Button: ({ children, ...props }: any) => , })); vi.mock('@alga-psa/ui/components/Badge', () => ({ Badge: ({ children }: any) => {children}, })); vi.mock('@alga-psa/ui/components/Input', () => ({ Input: ({ containerClassName: _containerClassName, ...props }: any) => , })); vi.mock('@alga-psa/ui/components/Checkbox', () => ({ // The component drives parent-row selection through onClick (for shift-range // support) and calls event.preventDefault(). On a native jsdom checkbox that // cancels the click activation and reverts `.checked`, so we hand the // component a no-op preventDefault instead. Checkbox: ({ indeterminate: _indeterminate, onClick, ...props }: any) => ( { onClick({ shiftKey: event.shiftKey, metaKey: event.metaKey, ctrlKey: event.ctrlKey, stopPropagation: () => event.stopPropagation(), preventDefault: () => {}, }); } : undefined } /> ), })); vi.mock('@alga-psa/ui/components/DateRangePicker', () => ({ DateRangePicker: () =>
, })); vi.mock('@alga-psa/ui/components/Alert', () => ({ Alert: ({ children }: any) =>
{children}
, AlertDescription: ({ children }: any) =>
{children}
, })); vi.mock('@alga-psa/ui/components/Dialog', () => ({ Dialog: ({ children }: any) =>
{children}
, DialogContent: ({ children }: any) =>
{children}
, DialogFooter: ({ children }: any) =>
{children}
, DialogDescription: ({ children }: any) =>
{children}
, })); vi.mock('@alga-psa/ui/components/DropdownMenu', () => ({ DropdownMenu: ({ children }: any) =>
{children}
, DropdownMenuContent: ({ children }: any) =>
{children}
, DropdownMenuItem: ({ children, ...props }: any) => , DropdownMenuSeparator: () =>
, DropdownMenuTrigger: ({ children }: any) =>
{children}
, })); vi.mock('@alga-psa/ui/components/ConfirmationDialog', () => ({ ConfirmationDialog: () => null, })); vi.mock('@alga-psa/ui/components/LoadingIndicator', () => ({ default: ({ text }: { text: string }) =>
{text}
, })); describe('AutomaticInvoices grouped parent rows', () => { beforeEach(() => { mockPreviewGroupedInvoicesForSelectionInputs.mockClear(); mockGenerateGroupedInvoicesAsRecurringBillingRun.mockClear(); mockDueWorkResponse = { invoiceCandidates: [ { candidateKey: 'invoice-candidate:client-1:2026-03-01:2026-04-01', clientId: 'client-1', clientName: 'Acme Co', windowStart: '2026-03-01', windowEnd: '2026-04-01', windowLabel: '2026-03-01 to 2026-04-01', servicePeriodStart: '2026-03-01', servicePeriodEnd: '2026-04-01', servicePeriodLabel: '2026-03-01 to 2026-04-01', cadenceOwners: ['contract'], cadenceSources: ['contract_anniversary'], contractId: 'contract-1', contractName: 'Main Contract', splitReasons: [], memberCount: 2, canGenerate: true, blockedReason: null, members: [ { executionIdentityKey: 'exec-1', canGenerate: true, billingCycleId: 'bc-1', clientId: 'client-1', purchaseOrderScopeKey: 'po-1', currencyCode: 'USD', taxSource: 'exclusive', exportShapeKey: 'shape-a', cadenceSource: 'contract_anniversary', duePosition: 'advance', servicePeriodLabel: '2026-03-01 to 2026-04-01', amountCents: 12500, selectorInput: { clientId: 'client-1', windowStart: '2026-03-01', windowEnd: '2026-04-01', executionWindow: { kind: 'contract_cadence_window', identityKey: 'contract-window:line-1:2026-03-01:2026-04-01', cadenceOwner: 'contract', contractId: 'contract-1', contractLineId: 'line-1', }, }, }, { executionIdentityKey: 'exec-2', canGenerate: true, billingCycleId: 'bc-2', clientId: 'client-1', purchaseOrderScopeKey: 'po-1', currencyCode: 'USD', taxSource: 'exclusive', exportShapeKey: 'shape-a', cadenceSource: 'contract_anniversary', duePosition: 'advance', servicePeriodLabel: '2026-03-01 to 2026-04-01', amountCents: 17500, selectorInput: { clientId: 'client-1', windowStart: '2026-03-01', windowEnd: '2026-04-01', executionWindow: { kind: 'contract_cadence_window', identityKey: 'contract-window:line-2:2026-03-01:2026-04-01', cadenceOwner: 'contract', contractId: 'contract-1', contractLineId: 'line-2', }, }, }, ], }, ], materializationGaps: [], total: 1, page: 1, pageSize: 10, totalPages: 1, }; mockRecurringInvoiceHistoryResponse = { rows: [], total: 0, page: 1, pageSize: 10 }; }); it('renders one parent group row for a shared client + invoice window instead of one top-level row per child (T001)', async () => { const AutomaticInvoices = (await import('../src/components/billing-dashboard/AutomaticInvoices')).default; render( undefined} />); await waitFor(() => { expect(screen.getByTestId('automatic-invoices-table-row-count')).toHaveTextContent('1'); }); expect(screen.getByText('Each parent row groups due obligations by client and invoice window. Child obligations remain the atomic execution units.')).toBeInTheDocument(); expect(screen.getByTestId('automatic-invoices-table')).toBeInTheDocument(); expect(screen.getAllByTestId('automatic-invoices-table-row')).toHaveLength(1); }); it('renders parent summary child count, aggregate amount, and invoice window (T002)', async () => { const AutomaticInvoices = (await import('../src/components/billing-dashboard/AutomaticInvoices')).default; render( undefined} />); await waitFor(() => { expect(screen.getByText('2 obligations')).toBeInTheDocument(); }); expect(screen.getAllByText('2026-03-01 to 2026-04-01').length).toBeGreaterThan(0); expect(screen.getByText('$300.00')).toBeInTheDocument(); }); it('expands a parent row to reveal child candidate details (T003)', async () => { const AutomaticInvoices = (await import('../src/components/billing-dashboard/AutomaticInvoices')).default; render( undefined} />); const expandButton = await screen.findByRole('button', { name: 'Expand' }); fireEvent.click(expandButton); expect( await screen.findByTestId('child-row-parent-group:client-1:2026-03-01:2026-04-01-exec-1'), ).toBeInTheDocument(); expect(screen.getAllByText('Assigned work item').length).toBeGreaterThan(0); expect(screen.getAllByText('Cadence: Contract anniversary').length).toBeGreaterThan(0); expect(screen.getAllByText('Billing timing: Advance').length).toBeGreaterThan(0); expect(screen.getAllByText('Service period: 2026-03-01 to 2026-04-01').length).toBeGreaterThan(0); expect(screen.getByText('$125.00')).toBeInTheDocument(); }); it('is combinable only when all ready children share client/currency/PO/tax/export scope (T004)', async () => { const AutomaticInvoices = (await import('../src/components/billing-dashboard/AutomaticInvoices')).default; render( undefined} />); await waitFor(() => { const checkbox = document.getElementById( 'select-parent-group:client-1:2026-03-01:2026-04-01', ) as HTMLInputElement | null; expect(checkbox).not.toBeNull(); expect(checkbox?.disabled).toBe(false); }); expect( screen.queryByTestId('combinability-reasons-parent-group:client-1:2026-03-01:2026-04-01'), ).not.toBeInTheDocument(); }); it('shows PO incompatibility reason when child PO scope differs (T005)', async () => { mockDueWorkResponse.invoiceCandidates[0].members[1].purchaseOrderScopeKey = 'po-2'; const AutomaticInvoices = (await import('../src/components/billing-dashboard/AutomaticInvoices')).default; render( undefined} />); await waitFor(() => { const checkbox = document.getElementById( 'select-parent-group:client-1:2026-03-01:2026-04-01', ) as HTMLInputElement | null; expect(checkbox).not.toBeNull(); expect(checkbox?.disabled).toBe(true); }); expect( screen.getByTestId('combinability-reasons-parent-group:client-1:2026-03-01:2026-04-01'), ).toHaveTextContent('PO scope differs'); }); it('shows currency incompatibility reason when child currency differs (T006)', async () => { mockDueWorkResponse.invoiceCandidates[0].members[1].currencyCode = 'EUR'; const AutomaticInvoices = (await import('../src/components/billing-dashboard/AutomaticInvoices')).default; render( undefined} />); await waitFor(() => { const checkbox = document.getElementById( 'select-parent-group:client-1:2026-03-01:2026-04-01', ) as HTMLInputElement | null; expect(checkbox).not.toBeNull(); expect(checkbox?.disabled).toBe(true); }); expect( screen.getByTestId('combinability-reasons-parent-group:client-1:2026-03-01:2026-04-01'), ).toHaveTextContent('Currency differs'); }); it('shows tax incompatibility reason when child tax source differs (T007)', async () => { mockDueWorkResponse.invoiceCandidates[0].members[1].taxSource = 'inclusive'; const AutomaticInvoices = (await import('../src/components/billing-dashboard/AutomaticInvoices')).default; render( undefined} />); await waitFor(() => { const checkbox = document.getElementById( 'select-parent-group:client-1:2026-03-01:2026-04-01', ) as HTMLInputElement | null; expect(checkbox).not.toBeNull(); expect(checkbox?.disabled).toBe(true); }); expect( screen.getByTestId('combinability-reasons-parent-group:client-1:2026-03-01:2026-04-01'), ).toHaveTextContent('Tax treatment differs'); }); it('shows export-shape incompatibility reason when child export shape differs (T008)', async () => { mockDueWorkResponse.invoiceCandidates[0].members[1].exportShapeKey = 'shape-b'; const AutomaticInvoices = (await import('../src/components/billing-dashboard/AutomaticInvoices')).default; render( undefined} />); await waitFor(() => { const checkbox = document.getElementById( 'select-parent-group:client-1:2026-03-01:2026-04-01', ) as HTMLInputElement | null; expect(checkbox).not.toBeNull(); expect(checkbox?.disabled).toBe(true); }); expect( screen.getByTestId('combinability-reasons-parent-group:client-1:2026-03-01:2026-04-01'), ).toHaveTextContent('Export shape differs'); }); it('selecting a combinable parent selects the full group target (T009)', async () => { const AutomaticInvoices = (await import('../src/components/billing-dashboard/AutomaticInvoices')).default; render( undefined} />); const parentCheckbox = await waitFor(() => { const checkbox = document.getElementById( 'select-parent-group:client-1:2026-03-01:2026-04-01', ) as HTMLInputElement | null; expect(checkbox).not.toBeNull(); return checkbox as HTMLInputElement; }); fireEvent.click(parentCheckbox); await waitFor(() => { expect(parentCheckbox.checked).toBe(true); }); expect(screen.getByText('Generate Invoices for Selected Periods (2)')).toBeInTheDocument(); }); it('non-combinable parent stays disabled while child rows remain selectable (T010)', async () => { mockDueWorkResponse.invoiceCandidates[0].members[1].currencyCode = 'EUR'; const AutomaticInvoices = (await import('../src/components/billing-dashboard/AutomaticInvoices')).default; render( undefined} />); const expandButton = await screen.findByRole('button', { name: 'Expand' }); fireEvent.click(expandButton); const parentCheckbox = document.getElementById( 'select-parent-group:client-1:2026-03-01:2026-04-01', ) as HTMLInputElement; const childCheckbox = document.getElementById( 'select-child-parent-group:client-1:2026-03-01:2026-04-01-exec-1', ) as HTMLInputElement; expect(parentCheckbox.disabled).toBe(true); expect(childCheckbox.disabled).toBe(false); }); it('partial child selection drives parent indeterminate state (T011)', async () => { const AutomaticInvoices = (await import('../src/components/billing-dashboard/AutomaticInvoices')).default; render( undefined} />); const expandButton = await screen.findByRole('button', { name: 'Expand' }); fireEvent.click(expandButton); const childCheckbox = document.getElementById( 'select-child-parent-group:client-1:2026-03-01:2026-04-01-exec-1', ) as HTMLInputElement; fireEvent.click(childCheckbox); const parentCheckbox = document.getElementById( 'select-parent-group:client-1:2026-03-01:2026-04-01', ) as HTMLInputElement; expect(parentCheckbox.checked).toBe(false); expect(parentCheckbox.dataset.indeterminate).toBe('true'); }); it('select all selects combinable groups by parent row (T012)', async () => { const AutomaticInvoices = (await import('../src/components/billing-dashboard/AutomaticInvoices')).default; render( undefined} />); const [selectAll] = await screen.findAllByRole('checkbox'); fireEvent.click(selectAll); const parentCheckbox = document.getElementById( 'select-parent-group:client-1:2026-03-01:2026-04-01', ) as HTMLInputElement; expect(parentCheckbox.checked).toBe(true); }); it('select all selects child rows for non-combinable groups (T013)', async () => { mockDueWorkResponse.invoiceCandidates[0].members[1].taxSource = 'inclusive'; const AutomaticInvoices = (await import('../src/components/billing-dashboard/AutomaticInvoices')).default; render( undefined} />); const [selectAll] = await screen.findAllByRole('checkbox'); fireEvent.click(selectAll); const expandButton = await screen.findByRole('button', { name: 'Expand' }); fireEvent.click(expandButton); const parentCheckbox = document.getElementById( 'select-parent-group:client-1:2026-03-01:2026-04-01', ) as HTMLInputElement; const childOne = document.getElementById( 'select-child-parent-group:client-1:2026-03-01:2026-04-01-exec-1', ) as HTMLInputElement; const childTwo = document.getElementById( 'select-child-parent-group:client-1:2026-03-01:2026-04-01-exec-2', ) as HTMLInputElement; expect(parentCheckbox.checked).toBe(false); expect(childOne.checked).toBe(true); expect(childTwo.checked).toBe(true); }); it('keeps blocked children visible but unselectable via child selection and select all (T014)', async () => { mockDueWorkResponse.invoiceCandidates[0].canGenerate = false; mockDueWorkResponse.invoiceCandidates[0].members[1].canGenerate = false; mockDueWorkResponse.invoiceCandidates[0].members[1].currencyCode = 'EUR'; const AutomaticInvoices = (await import('../src/components/billing-dashboard/AutomaticInvoices')).default; render( undefined} />); const [selectAll] = await screen.findAllByRole('checkbox'); fireEvent.click(selectAll); const expandButton = await screen.findByRole('button', { name: 'Expand' }); fireEvent.click(expandButton); const blockedChild = document.getElementById( 'select-child-parent-group:client-1:2026-03-01:2026-04-01-exec-2', ) as HTMLInputElement; const readyChild = document.getElementById( 'select-child-parent-group:client-1:2026-03-01:2026-04-01-exec-1', ) as HTMLInputElement; expect( screen.getByTestId('child-row-parent-group:client-1:2026-03-01:2026-04-01-exec-2'), ).toBeInTheDocument(); expect(screen.getByText('Contains blocked items')).toBeInTheDocument(); expect(screen.queryByText('Must invoice separately')).not.toBeInTheDocument(); expect(blockedChild.disabled).toBe(true); expect(blockedChild.checked).toBe(false); expect(readyChild.checked).toBe(true); }); it('previewing a selected combinable parent renders one combined invoice preview count (T015)', async () => { const AutomaticInvoices = (await import('../src/components/billing-dashboard/AutomaticInvoices')).default; render( undefined} />); const parentCheckbox = await waitFor(() => { const checkbox = document.getElementById( 'select-parent-group:client-1:2026-03-01:2026-04-01', ) as HTMLInputElement | null; expect(checkbox).not.toBeNull(); return checkbox as HTMLInputElement; }); fireEvent.click(parentCheckbox); fireEvent.click(screen.getByRole('button', { name: 'Preview Selected' })); await waitFor(() => { expect(screen.getByTestId('preview-invoice-count-summary')).toHaveTextContent( 'This selection will generate one combined invoice.', ); }); expect(mockPreviewGroupedInvoicesForSelectionInputs).toHaveBeenCalledTimes(1); const previewPayload = mockPreviewGroupedInvoicesForSelectionInputs.mock.calls[0][0]; expect(previewPayload).toHaveLength(1); expect(previewPayload[0].selectorInputs).toHaveLength(2); }); it('previewing mixed child selection renders multi-invoice preview count (T016)', async () => { mockDueWorkResponse.invoiceCandidates[0].members[1].currencyCode = 'EUR'; const AutomaticInvoices = (await import('../src/components/billing-dashboard/AutomaticInvoices')).default; render( undefined} />); const expandButton = await screen.findByRole('button', { name: 'Expand' }); fireEvent.click(expandButton); const childOne = document.getElementById( 'select-child-parent-group:client-1:2026-03-01:2026-04-01-exec-1', ) as HTMLInputElement; const childTwo = document.getElementById( 'select-child-parent-group:client-1:2026-03-01:2026-04-01-exec-2', ) as HTMLInputElement; fireEvent.click(childOne); fireEvent.click(childTwo); fireEvent.click(screen.getByRole('button', { name: 'Preview Selected' })); await waitFor(() => { expect(screen.getByTestId('preview-invoice-count-summary')).toHaveTextContent( 'This selection will generate 2 separate invoices.', ); }); }); it('preview request uses exact selected child scope without unselected siblings (T017)', async () => { const AutomaticInvoices = (await import('../src/components/billing-dashboard/AutomaticInvoices')).default; render( undefined} />); const expandButton = await screen.findByRole('button', { name: 'Expand' }); fireEvent.click(expandButton); const childOne = document.getElementById( 'select-child-parent-group:client-1:2026-03-01:2026-04-01-exec-1', ) as HTMLInputElement; fireEvent.click(childOne); fireEvent.click(screen.getByRole('button', { name: 'Preview Selected' })); await waitFor(() => { expect(mockPreviewGroupedInvoicesForSelectionInputs).toHaveBeenCalledTimes(1); }); const previewPayload = mockPreviewGroupedInvoicesForSelectionInputs.mock.calls[0][0]; expect(previewPayload).toHaveLength(1); expect(previewPayload[0].selectorInputs).toHaveLength(1); expect(previewPayload[0].selectorInputs[0].executionWindow.contractLineId).toBe('line-1'); }); it('generation payload does not re-expand into unselected siblings from the same group (T020)', async () => { mockDueWorkResponse.invoiceCandidates[0].members.push({ executionIdentityKey: 'exec-3', canGenerate: true, billingCycleId: 'bc-3', clientId: 'client-1', purchaseOrderScopeKey: 'po-1', currencyCode: 'USD', taxSource: 'exclusive', exportShapeKey: 'shape-a', cadenceSource: 'contract_anniversary', duePosition: 'advance', servicePeriodLabel: '2026-03-01 to 2026-04-01', amountCents: 5000, selectorInput: { clientId: 'client-1', windowStart: '2026-03-01', windowEnd: '2026-04-01', executionWindow: { kind: 'contract_cadence_window', identityKey: 'contract-window:line-3:2026-03-01:2026-04-01', cadenceOwner: 'contract', contractId: 'contract-1', contractLineId: 'line-3', }, }, }); mockDueWorkResponse.invoiceCandidates[0].memberCount = 3; const AutomaticInvoices = (await import('../src/components/billing-dashboard/AutomaticInvoices')).default; render( undefined} />); const expandButton = await screen.findByRole('button', { name: 'Expand' }); fireEvent.click(expandButton); const childOne = document.getElementById( 'select-child-parent-group:client-1:2026-03-01:2026-04-01-exec-1', ) as HTMLInputElement; fireEvent.click(childOne); fireEvent.click(screen.getByRole('button', { name: 'Generate Invoices for Selected Periods (1)' })); await waitFor(() => { expect(mockGenerateGroupedInvoicesAsRecurringBillingRun).toHaveBeenCalledTimes(1); }); const generationPayload = mockGenerateGroupedInvoicesAsRecurringBillingRun.mock.calls[0][0]; expect(generationPayload.groupedTargets).toHaveLength(1); expect(generationPayload.groupedTargets[0].selectorInputs).toHaveLength(1); expect(generationPayload.groupedTargets[0].selectorInputs[0].executionWindow.contractLineId).toBe('line-1'); }); it('keeps parent non-combinable when PO scope differs across child candidates (T026)', async () => { mockDueWorkResponse.invoiceCandidates[0].members[1].purchaseOrderScopeKey = 'po-2'; const AutomaticInvoices = (await import('../src/components/billing-dashboard/AutomaticInvoices')).default; render( undefined} />); await waitFor(() => { const parentCheckbox = document.getElementById( 'select-parent-group:client-1:2026-03-01:2026-04-01', ) as HTMLInputElement | null; expect(parentCheckbox).not.toBeNull(); expect(parentCheckbox?.disabled).toBe(true); }); expect( screen.getByTestId('combinability-reasons-parent-group:client-1:2026-03-01:2026-04-01'), ).toHaveTextContent('PO scope differs'); }); it('legacy single-assignment/single-child groups still generate through the existing flow (T028/T029)', async () => { mockDueWorkResponse.invoiceCandidates[0].members = [mockDueWorkResponse.invoiceCandidates[0].members[0]]; mockDueWorkResponse.invoiceCandidates[0].memberCount = 1; const AutomaticInvoices = (await import('../src/components/billing-dashboard/AutomaticInvoices')).default; render( undefined} />); const parentCheckbox = await waitFor(() => { const checkbox = document.getElementById( 'select-parent-group:client-1:2026-03-01:2026-04-01', ) as HTMLInputElement | null; expect(checkbox).not.toBeNull(); return checkbox as HTMLInputElement; }); fireEvent.click(parentCheckbox); fireEvent.click(screen.getByRole('button', { name: 'Preview Selected' })); await waitFor(() => { expect(screen.getByTestId('preview-invoice-count-summary')).toHaveTextContent( 'This selection will generate one combined invoice.', ); }); expect(screen.queryByTestId('grouped-preview-unavailable-copy')).not.toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: 'Generate Invoices for Selected Periods (1)' })); await waitFor(() => { expect(mockGenerateGroupedInvoicesAsRecurringBillingRun).toHaveBeenCalledTimes(1); }); const payload = mockGenerateGroupedInvoicesAsRecurringBillingRun.mock.calls[0][0]; expect(payload.groupedTargets).toHaveLength(1); expect(payload.groupedTargets[0].selectorInputs).toHaveLength(1); }); it('renders recurring history assignment scope summary and multi-contract badge for combined invoices (T024/T025)', async () => { mockRecurringInvoiceHistoryResponse = { rows: [ { invoiceId: 'invoice-1', invoiceNumber: 'INV-1001', invoiceStatus: 'draft', invoiceDate: '2026-03-08', billingCycleId: null, hasBillingCycleBridge: false, clientId: 'client-1', clientName: 'Acme Co', cadenceSource: 'contract_anniversary', servicePeriodStart: '2026-03-01', servicePeriodEnd: '2026-04-01', servicePeriodLabel: '2026-03-01 to 2026-04-01', invoiceWindowStart: '2026-03-01', invoiceWindowEnd: '2026-04-01', invoiceWindowLabel: '2026-03-01 to 2026-04-01', assignmentContractIds: ['assignment-1', 'assignment-2'], isMultiAssignment: true, assignmentSummary: 'Multi-assignment invoice (2)', }, ], total: 1, page: 1, pageSize: 10, }; const AutomaticInvoices = (await import('../src/components/billing-dashboard/AutomaticInvoices')).default; render( undefined} />); await waitFor(() => { expect(screen.getByTestId('already-invoiced-table-row-count')).toHaveTextContent('1'); }); expect(screen.getByText('Multi-assignment invoice (2)')).toBeInTheDocument(); expect(screen.getByText('Multi-contract invoice')).toBeInTheDocument(); }); });