/** * @vitest-environment jsdom */ import React from 'react'; import '@testing-library/jest-dom/vitest'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { act, cleanup, render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; const mockGetDefaultBillingSettings = vi.fn(); const mockUpdateDefaultBillingSettings = vi.fn(); const mockGetAllBoards = vi.fn(); const mockGetTicketStatuses = vi.fn(); const mockHandleError = vi.fn(); vi.mock('@alga-psa/billing/actions', () => ({ getDefaultBillingSettings: (...args: unknown[]) => mockGetDefaultBillingSettings(...args), updateDefaultBillingSettings: (...args: unknown[]) => mockUpdateDefaultBillingSettings(...args), })); // The component imports getAllBoards from reference-data/actions, not // tickets/actions — mock the exact specifier the code under test imports. vi.mock('@alga-psa/reference-data/actions', () => ({ getAllBoards: (...args: unknown[]) => mockGetAllBoards(...args), getTicketStatuses: (...args: unknown[]) => mockGetTicketStatuses(...args), })); // Without this mock, react-i18next (never initialized in tests) returns a new // `t` per render; the component's load effect depends on [t] and sets fresh // array state, so rendering spins forever in microtasks — starving vitest's // test timeout timer and hanging the suite at 100% CPU. vi.mock('@alga-psa/ui/lib/i18n/client', () => { const t = (key: string, opts?: { defaultValue?: string }) => opts?.defaultValue ?? key; return { useTranslation: () => ({ t }) }; }); vi.mock('react-hot-toast', () => ({ default: { success: vi.fn(), }, })); vi.mock('@alga-psa/ui/lib/errorHandling', () => ({ handleError: (...args: unknown[]) => mockHandleError(...args), isActionPermissionError: () => false, })); vi.mock('@alga-psa/ui/components/CustomSelect', () => ({ __esModule: true, default: ({ id, options = [], value, onValueChange, placeholder, disabled, }: any) => ( ), })); describe('RenewalAutomationSettings', () => { beforeEach(() => { cleanup(); vi.clearAllMocks(); mockGetDefaultBillingSettings.mockResolvedValue({ zeroDollarInvoiceHandling: 'normal', suppressZeroDollarInvoices: false, enableCreditExpiration: true, creditExpirationDays: 365, creditExpirationNotificationDays: [30, 7, 1], defaultRenewalMode: 'manual', defaultNoticePeriodDays: 30, renewalDueDateActionPolicy: 'create_ticket', renewalTicketBoardId: undefined, renewalTicketStatusId: undefined, renewalTicketPriority: undefined, renewalTicketAssigneeId: undefined, }); mockUpdateDefaultBillingSettings.mockResolvedValue({ success: true }); mockGetAllBoards.mockResolvedValue([ { board_id: 'board-1', board_name: 'Service Desk' }, { board_id: 'board-2', board_name: 'Projects' }, ]); mockGetTicketStatuses.mockImplementation(async (boardId: string) => { if (boardId === 'board-1') { return [ { status_id: 'status-1', name: 'New' }, { status_id: 'status-2', name: 'In Progress' }, ]; } if (boardId === 'board-2') { return [{ status_id: 'status-3', name: 'Planned' }]; } return []; }); }); it('loads statuses for the selected board only and clears stale status selections when the board changes', async () => { const { default: RenewalAutomationSettings } = await import( '../src/components/settings/billing/RenewalAutomationSettings' ); const user = userEvent.setup(); await act(async () => { render(); }); const boardSelect = await screen.findByTestId('renewal-ticket-board-select'); const statusSelect = await screen.findByTestId('renewal-ticket-status-select'); expect(statusSelect).toBeDisabled(); await act(async () => { await user.selectOptions(boardSelect, 'board-1'); }); await waitFor(() => expect(mockGetTicketStatuses).toHaveBeenLastCalledWith('board-1')); expect(statusSelect).not.toBeDisabled(); expect(screen.getByRole('option', { name: 'New' })).toBeInTheDocument(); expect(screen.queryByRole('option', { name: 'Planned' })).not.toBeInTheDocument(); await act(async () => { await user.selectOptions(statusSelect, 'status-2'); }); expect((statusSelect as HTMLSelectElement).value).toBe('status-2'); await act(async () => { await user.selectOptions(boardSelect, 'board-2'); }); await waitFor(() => expect(mockGetTicketStatuses).toHaveBeenLastCalledWith('board-2')); expect((statusSelect as HTMLSelectElement).value).toBe(''); expect(screen.getByRole('option', { name: 'Planned' })).toBeInTheDocument(); expect(screen.queryByRole('option', { name: 'In Progress' })).not.toBeInTheDocument(); await act(async () => { await user.click(screen.getByRole('button', { name: 'Save' })); }); expect(mockUpdateDefaultBillingSettings).toHaveBeenCalledWith( expect.objectContaining({ renewalTicketBoardId: 'board-2', renewalTicketStatusId: undefined, }) ); }); });