/** * @vitest-environment jsdom */ import React from 'react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { act, render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; const createContractTemplateFromWizard = vi.fn(async () => ({ contract_id: 'template-1' })); const checkTemplateNameExists = vi.fn(async () => false); vi.mock('@alga-psa/ui/components/Dialog', () => ({ // The wizard renders WizardNavigation through the Dialog footer prop now, // so the mock must render the footer alongside the children. Dialog: ({ isOpen, children, footer, }: { isOpen: boolean; children: React.ReactNode; footer?: React.ReactNode; }) => isOpen ? React.createElement('div', { 'data-testid': 'dialog' }, children, footer) : null, })); vi.mock('@alga-psa/ui/components/onboarding/WizardProgress', () => ({ WizardProgress: ({ currentStep }: { currentStep: number }) => React.createElement('div', { 'data-testid': 'wizard-progress', 'data-current-step': String(currentStep), }), })); vi.mock('@alga-psa/ui/components/onboarding/WizardNavigation', () => ({ WizardNavigation: ({ onNext, onBack, onFinish, }: { onNext: () => void; onBack: () => void; onFinish: () => void; }) => React.createElement( 'div', {}, React.createElement( 'button', { type: 'button', onClick: onBack, }, 'Back' ), React.createElement( 'button', { type: 'button', onClick: onNext, }, 'Next' ), React.createElement( 'button', { type: 'button', onClick: onFinish, }, 'Finish' ) ), })); vi.mock('@alga-psa/billing/actions/contractWizardActions', () => ({ createContractTemplateFromWizard: (...args: any[]) => createContractTemplateFromWizard(...args), checkTemplateNameExists: (...args: any[]) => checkTemplateNameExists(...args), })); vi.mock('../src/components/billing-dashboard/contracts/template-wizard/steps/TemplateContractBasicsStep', () => ({ TemplateContractBasicsStep: ({ updateData }: { updateData: (data: any) => void }) => { React.useEffect(() => { updateData({ contract_name: 'Bucket-Enabled Template', billing_frequency: 'monthly', }); // Intentionally initialize once per mount for test setup. // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return React.createElement('div', { 'data-testid': 'step-basics' }); }, })); vi.mock('../src/components/billing-dashboard/contracts/template-wizard/steps/TemplateFixedFeeServicesStep', () => ({ TemplateFixedFeeServicesStep: ({ updateData }: { updateData: (data: any) => void }) => { React.useEffect(() => { updateData({ cadence_owner: 'client', billing_timing: 'advance', enable_proration: true, fixed_services: [ { service_id: 'svc-fixed-1', service_name: 'Fixed Service', quantity: 1, }, ], }); // Intentionally initialize once per mount for test setup. // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return React.createElement('div', { 'data-testid': 'step-fixed' }); }, })); vi.mock('../src/components/billing-dashboard/contracts/template-wizard/steps/TemplateProductsStep', () => ({ TemplateProductsStep: () => React.createElement('div', { 'data-testid': 'step-products' }), })); vi.mock('../src/components/billing-dashboard/contracts/template-wizard/steps/TemplateHourlyServicesStep', () => ({ TemplateHourlyServicesStep: ({ updateData }: { updateData: (data: any) => void }) => { React.useEffect(() => { updateData({ hourly_services: [ { service_id: 'svc-hourly-1', service_name: 'Hourly Service', bucket_overlay: { total_minutes: 120, overage_rate: 15000, allow_rollover: true, billing_period: 'monthly', }, }, ], }); // Intentionally initialize once per mount for test setup. // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return React.createElement('div', { 'data-testid': 'step-hourly' }); }, })); vi.mock('../src/components/billing-dashboard/contracts/template-wizard/steps/TemplateUsageBasedServicesStep', () => ({ TemplateUsageBasedServicesStep: ({ updateData }: { updateData: (data: any) => void }) => { React.useEffect(() => { updateData({ usage_services: [ { service_id: 'svc-usage-1', service_name: 'Usage Service', unit_of_measure: 'seat', bucket_overlay: { total_minutes: 45, overage_rate: 2200, allow_rollover: false, billing_period: 'weekly', }, }, ], }); // Intentionally initialize once per mount for test setup. // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return React.createElement('div', { 'data-testid': 'step-usage' }); }, })); vi.mock('../src/components/billing-dashboard/contracts/template-wizard/steps/TemplateReviewContractStep', () => ({ TemplateReviewContractStep: () => React.createElement('div', { 'data-testid': 'step-review' }), })); describe('TemplateWizard bucket overlays', () => { beforeEach(() => { vi.clearAllMocks(); createContractTemplateFromWizard.mockResolvedValue({ contract_id: 'template-1' }); checkTemplateNameExists.mockResolvedValue(false); }); it('submits hourly and usage bucket overlays from template wizard data', async () => { const { TemplateWizard } = await import( '../src/components/billing-dashboard/contracts/template-wizard/TemplateWizard' ); const user = userEvent.setup(); render(React.createElement(TemplateWizard, { open: true, onOpenChange: vi.fn(), onComplete: vi.fn() })); await screen.findByTestId('step-basics'); await act(async () => { await user.click(screen.getByText('Next')); }); await act(async () => { await user.click(screen.getByText('Next')); }); await act(async () => { await user.click(screen.getByText('Next')); }); await screen.findByTestId('step-hourly'); await act(async () => { await user.click(screen.getByText('Next')); }); await screen.findByTestId('step-usage'); await act(async () => { await user.click(screen.getByText('Next')); }); await screen.findByTestId('step-review'); await act(async () => { await user.click(screen.getByText('Finish')); }); await waitFor(() => { expect(createContractTemplateFromWizard).toHaveBeenCalledTimes(1); }); const submitted = createContractTemplateFromWizard.mock.calls[0][0]; expect(submitted.cadence_owner).toBe('client'); expect(submitted.billing_timing).toBe('advance'); expect(submitted.enable_proration).toBe(true); expect(submitted.hourly_services?.[0]?.bucket_overlay).toEqual({ total_minutes: 120, overage_rate: 15000, allow_rollover: true, billing_period: 'monthly', }); expect(submitted.usage_services?.[0]?.bucket_overlay).toEqual({ total_minutes: 45, overage_rate: 2200, allow_rollover: false, billing_period: 'weekly', }); }); });