import { afterEach, describe, expect, it, vi } from 'vitest'; import { render, screen, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import SettingsPage from '@/app/(dashboard)/settings/settings-page-client'; function renderScheduledCancellation(status: 'ACTIVE' | 'TRIALING') { vi.stubGlobal( 'fetch', vi.fn(async (url: string) => { if (url !== '/api/billing') return { ok: false }; return { ok: true, json: async () => ({ data: { isEnabled: true, isConfigured: true, checkoutAvailable: false, portalAvailable: true, cancelAvailable: false, needsPaymentFix: false, openInvoice: null, workspaceCreation: { canCreateWorkspace: true, canStartTrial: false }, subscription: { status, label: status === 'ACTIVE' ? 'Active' : 'Trialing', hasActiveSubscription: true, hasRecoverableSubscription: true, hasActiveTrial: true, hasBillingAccess: true, currentPeriodEnd: '2026-10-08T12:00:00Z', trialEndsAt: '2026-09-15T12:00:00Z', cancelAtPeriodEnd: true, cancelAt: '2026-10-08T12:00:00Z', }, }, }), }; }) ); render(); } afterEach(() => vi.unstubAllGlobals()); describe('scheduled cancellation in billing settings', () => { it('keeps a paid subscription distinct from its remaining cardless trial', async () => { renderScheduledCancellation('ACTIVE'); expect( await screen.findByText( 'Subscription canceled. Access remains active until the end of the current billing period.' ) ).toBeInTheDocument(); expect(screen.queryByText(/Trial canceled/)).not.toBeInTheDocument(); expect(screen.queryByText(/Access ends on/)).not.toBeInTheDocument(); expect(screen.getByText(/Your subscription ends on/)).toHaveTextContent( new Date('2026-10-08T12:00:00Z').toLocaleDateString() ); expect(screen.getByText(/Cancellation takes effect on/)).toBeInTheDocument(); expect(screen.queryByText(/Cancellation was scheduled on/)).not.toBeInTheDocument(); }); it('still explains the trial end for a Stripe trial subscription', async () => { renderScheduledCancellation('TRIALING'); expect( await screen.findByText('Trial canceled. Access remains active until the trial ends.') ).toBeInTheDocument(); expect(screen.getByText(/Access ends on/)).toHaveTextContent( new Date('2026-09-15T12:00:00Z').toLocaleDateString() ); }); }); function renderPastDue(hasBillingAccess: boolean) { vi.stubGlobal( 'fetch', vi.fn(async (url: string) => { if (url !== '/api/billing') return { ok: false }; return { ok: true, json: async () => ({ data: { isEnabled: true, isConfigured: true, checkoutAvailable: false, portalAvailable: true, cancelAvailable: true, cancelIsImmediate: true, needsPaymentFix: true, openInvoice: null, workspaceCreation: { canCreateWorkspace: hasBillingAccess, canStartTrial: false }, subscription: { status: 'PAST_DUE', label: 'Past due', hasActiveSubscription: false, hasRecoverableSubscription: true, hasActiveTrial: false, hasBillingAccess, currentPeriodEnd: '2026-10-08T12:00:00Z', trialEndsAt: null, cancelAtPeriodEnd: false, cancelAt: null, }, }, }), }; }) ); render(); } describe('past-due access in billing settings', () => { it('opens immediate unpaid cancellation copy and waits for confirmation', async () => { const user = userEvent.setup(); renderPastDue(true); await user.click(await screen.findByRole('button', { name: 'Cancel subscription' })); const dialog = within(screen.getByRole('dialog')); expect(dialog.getByText(/This subscription ends immediately\./)).toHaveTextContent( 'Canceling does not extend access to your workspaces.' ); expect(dialog.getByText(/Automatic collection stops/)).toHaveTextContent( 'charges for prior service and other items may still be owed.' ); expect(dialog.queryByText(/Everything stays on until/)).not.toBeInTheDocument(); expect(dialog.getByRole('button', { name: 'Cancel subscription' })).toBeEnabled(); expect(vi.mocked(fetch).mock.calls.some(([url]) => url === '/api/billing/cancel')).toBe(false); await user.click(dialog.getByRole('button', { name: 'Keep subscription' })); expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); expect(vi.mocked(fetch).mock.calls.some(([url]) => url === '/api/billing/cancel')).toBe(false); }); it('shows continued workspace access during payment grace without an active trial', async () => { renderPastDue(true); expect( await screen.findByText('Workspace access remains available while you resolve your payment.') ).toBeInTheDocument(); expect(screen.queryByText('Billing access has ended.')).not.toBeInTheDocument(); expect(screen.queryByText('Free trial, no card required.')).not.toBeInTheDocument(); }); it('shows access has ended when payment grace has expired and no trial remains', async () => { renderPastDue(false); expect(await screen.findByText('Billing access has ended.')).toBeInTheDocument(); expect( screen.queryByText('Workspace access remains available while you resolve your payment.') ).not.toBeInTheDocument(); expect(screen.queryByText('Free trial, no card required.')).not.toBeInTheDocument(); }); });