import { describe, it, expect, beforeEach, vi } from 'vitest' import { render, screen, fireEvent, waitFor } from '@testing-library/react' import { MemoryRouter } from 'react-router-dom' import { BillingPage } from '../BillingPage' import { useBillingStore } from '@/store/billingStore' import { BillingPortalError } from '@/types/billing' import type { SubscriptionState, PlanBillingState } from '@/types/billing' vi.mock('@/api/billing', () => ({ billingApi: { getPortalSession: vi.fn(), }, })) vi.mock('@/lib/toast', () => ({ toast: { error: vi.fn(), success: vi.fn(), info: vi.fn(), warning: vi.fn(), }, })) import { billingApi } from '@/api/billing' import { toast } from '@/lib/toast' const getPortalSession = billingApi.getPortalSession as unknown as ReturnType const toastError = toast.error as unknown as ReturnType function setBilling(opts: { subscription: SubscriptionState | null planBilling?: PlanBillingState | null }) { useBillingStore.setState({ subscription: opts.subscription, planBilling: opts.planBilling ?? ({ display_name: 'Pro', description: 'Pro plan', monthly_price_cents: 4900, annual_price_cents: 49000, } as PlanBillingState), planLimits: {}, enabledFeatures: {}, isLoading: false, error: null, }) } function renderPage() { return render( , ) } describe('BillingPage', () => { beforeEach(() => { getPortalSession.mockReset() toastError.mockReset() useBillingStore.setState({ subscription: null, planBilling: null, planLimits: {}, enabledFeatures: {}, isLoading: false, error: null, }) }) it('renders subscription summary from useBillingStore', () => { setBilling({ subscription: { status: 'active', plan: 'pro', current_period_start: '2026-04-01T00:00:00Z', current_period_end: '2026-05-01T00:00:00Z', cancel_at_period_end: false, seat_limit: 5, has_pro_entitlement: true, is_paid: true, }, }) renderPage() expect(screen.getByRole('heading', { name: 'Billing' })).toBeInTheDocument() expect(screen.getByText('Pro')).toBeInTheDocument() expect(screen.getByText('Active')).toBeInTheDocument() // Seats shown expect(screen.getByText('5')).toBeInTheDocument() }) it('shows trial-ends message + Pick a plan CTA when trialing', () => { setBilling({ subscription: { status: 'trialing', plan: 'pro', current_period_start: '2026-04-22T00:00:00Z', current_period_end: '2026-05-06T00:00:00Z', cancel_at_period_end: false, seat_limit: 5, has_pro_entitlement: true, is_paid: false, }, }) renderPage() expect(screen.getByTestId('trial-message').textContent).toMatch(/Trial ends/) const pickPlan = screen.getByTestId('select-plan-link') expect(pickPlan.getAttribute('href')).toBe('/account/billing/select-plan') }) it('shows past-due banner with update payment CTA when status=past_due', () => { setBilling({ subscription: { status: 'past_due', plan: 'pro', current_period_start: '2026-04-01T00:00:00Z', current_period_end: '2026-05-01T00:00:00Z', cancel_at_period_end: false, seat_limit: 5, has_pro_entitlement: false, is_paid: true, }, }) renderPage() expect(screen.getByTestId('past-due-banner')).toBeInTheDocument() expect(screen.getByTestId('past-due-update-payment')).toBeInTheDocument() }) it('renders complimentary message and hides CTAs when complimentary', () => { setBilling({ subscription: { status: 'complimentary', plan: 'pro', current_period_start: '2026-04-01T00:00:00Z', current_period_end: null, cancel_at_period_end: false, seat_limit: null, has_pro_entitlement: true, is_paid: true, }, }) renderPage() expect(screen.getByTestId('complimentary-message')).toBeInTheDocument() expect(screen.queryByTestId('manage-billing-button')).not.toBeInTheDocument() expect(screen.queryByTestId('select-plan-link')).not.toBeInTheDocument() expect(screen.queryByTestId('change-plan-link')).not.toBeInTheDocument() }) it('renders canceled message + Pick a plan CTA when canceled', () => { setBilling({ subscription: { status: 'canceled', plan: 'pro', current_period_start: '2026-03-01T00:00:00Z', current_period_end: '2026-04-01T00:00:00Z', cancel_at_period_end: false, seat_limit: 5, has_pro_entitlement: false, is_paid: false, }, }) renderPage() expect(screen.getByTestId('canceled-message')).toBeInTheDocument() expect(screen.getByTestId('select-plan-link')).toBeInTheDocument() }) it('shows toast when portal session fails with no_stripe_customer', async () => { setBilling({ subscription: { status: 'active', plan: 'pro', current_period_start: '2026-04-01T00:00:00Z', current_period_end: '2026-05-01T00:00:00Z', cancel_at_period_end: false, seat_limit: 5, has_pro_entitlement: true, is_paid: true, }, }) getPortalSession.mockRejectedValueOnce( new BillingPortalError('no_stripe_customer'), ) renderPage() fireEvent.click(screen.getByTestId('manage-billing-button')) await waitFor(() => { expect(toastError).toHaveBeenCalledWith( 'Complete checkout first to access billing portal.', ) }) }) })