/* eslint-disable import-x/order */ import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { beforeEach, describe, expect, it, vi } from 'vitest'; // Mock lucide-react icons vi.mock('lucide-react', () => ({ AlertCircle: (props: Record) => , CreditCard: (props: Record) => , Loader2: (props: Record) => , Smartphone: (props: Record) => , Wallet: (props: Record) => , })); const mockCreatePayment = vi.fn(); const mockCreateSubscription = vi.fn(); const mockUpgradeSubscription = vi.fn(); vi.mock('@/lib/payment-api', () => ({ paymentApi: { createPayment: (...args: unknown[]) => mockCreatePayment(...args), }, })); vi.mock('@/lib/subscription-api', () => ({ subscriptionApi: { createSubscription: (...args: unknown[]) => mockCreateSubscription(...args), upgradeSubscription: (...args: unknown[]) => mockUpgradeSubscription(...args), }, })); vi.mock('@/lib/currency', () => ({ formatVND: (amount: string | number) => `${Number(amount).toLocaleString()} đ`, })); vi.mock('@/components/error-boundary', () => ({ ComponentErrorBoundary: ({ children }: { children: React.ReactNode }) => <>{children}, })); import { CheckoutModal } from '../checkout-modal'; const basePlan = { id: 'plan-1', tier: 'AGENT_PRO', name: 'Môi giới Pro', priceMonthlyVND: '499000', priceYearlyVND: '4990000', maxListings: 50, maxSavedSearches: 10, features: {}, isActive: true, }; describe('CheckoutModal', () => { const onOpenChange = vi.fn(); beforeEach(() => { vi.clearAllMocks(); mockCreatePayment.mockResolvedValue({ paymentUrl: 'https://vnpay.vn/pay', paymentId: 'p1', providerTxId: 'tx1' }); mockCreateSubscription.mockResolvedValue({ subscriptionId: 's1' }); mockUpgradeSubscription.mockResolvedValue({ subscriptionId: 's1' }); // Mock window.location Object.defineProperty(window, 'location', { writable: true, value: { ...window.location, href: 'http://localhost:3000/vi/pricing', origin: 'http://localhost:3000', pathname: '/vi/pricing' }, }); }); it('renders nothing when plan is null', () => { const { container } = render( , ); expect(container.querySelector('[role="dialog"]')).not.toBeInTheDocument(); }); it('renders order summary with plan name and monthly price', () => { render( , ); expect(screen.getByText('Đăng ký gói dịch vụ')).toBeInTheDocument(); expect(screen.getAllByText('Môi giới Pro').length).toBeGreaterThanOrEqual(1); expect(screen.getByText('Hàng tháng')).toBeInTheDocument(); expect(screen.getAllByText(/499,000/).length).toBeGreaterThanOrEqual(1); }); it('renders yearly badge and yearly price', () => { render( , ); expect(screen.getByText('Hàng năm')).toBeInTheDocument(); expect(screen.getByText('-17%')).toBeInTheDocument(); expect(screen.getAllByText(/4,990,000/).length).toBeGreaterThanOrEqual(1); }); it('renders upgrade title when isUpgrade=true', () => { render( , ); expect(screen.getByText('Nâng cấp gói dịch vụ')).toBeInTheDocument(); expect(screen.getByText(/Miễn phí/)).toBeInTheDocument(); }); it('renders all three payment providers', () => { render( , ); expect(screen.getByText('VNPay')).toBeInTheDocument(); expect(screen.getByText('MoMo')).toBeInTheDocument(); expect(screen.getByText('ZaloPay')).toBeInTheDocument(); }); it('selects a different payment provider on click', async () => { render( , ); const momoButton = screen.getByText('MoMo').closest('button')!; await userEvent.click(momoButton); // MoMo button should now have primary styling (ring-1 ring-primary) expect(momoButton.className).toContain('border-primary'); }); it('calls createSubscription + createPayment on checkout', async () => { render( , ); await userEvent.click(screen.getByRole('button', { name: /thanh toán/i })); await waitFor(() => { expect(mockCreateSubscription).toHaveBeenCalledWith('AGENT_PRO', 'monthly'); expect(mockCreatePayment).toHaveBeenCalledWith( expect.objectContaining({ provider: 'VNPAY', type: 'SUBSCRIPTION', amountVND: 499000, returnUrl: 'http://localhost:3000/vi/payment/return', }), ); }); }); it('uses the locale root payment return route from dashboard checkout', async () => { Object.defineProperty(window, 'location', { writable: true, value: { ...window.location, href: 'http://localhost:3000/vi/dashboard/subscription', origin: 'http://localhost:3000', pathname: '/vi/dashboard/subscription', }, }); render( , ); await userEvent.click(screen.getByRole('button', { name: /thanh toán/i })); await waitFor(() => { expect(mockCreatePayment).toHaveBeenCalledWith( expect.objectContaining({ returnUrl: 'http://localhost:3000/vi/payment/return', }), ); }); }); it('calls upgradeSubscription instead of createSubscription for upgrades', async () => { render( , ); await userEvent.click(screen.getByRole('button', { name: /thanh toán/i })); await waitFor(() => { expect(mockUpgradeSubscription).toHaveBeenCalledWith('AGENT_PRO'); expect(mockCreateSubscription).not.toHaveBeenCalled(); }); }); it('displays error message when payment fails', async () => { mockCreateSubscription.mockRejectedValue(new Error('Hệ thống bận')); render( , ); await userEvent.click(screen.getByRole('button', { name: /thanh toán/i })); await waitFor(() => { expect(screen.getByText('Hệ thống bận')).toBeInTheDocument(); }); }); it('disables close button and providers while processing', async () => { // Make the payment hang mockCreateSubscription.mockReturnValue(new Promise(() => {})); render( , ); await userEvent.click(screen.getByRole('button', { name: /thanh toán/i })); await waitFor(() => { expect(screen.getByText(/đang xử lý/i)).toBeInTheDocument(); expect(screen.getByRole('button', { name: /hủy/i })).toBeDisabled(); }); }); it('dismiss error on close button click', async () => { mockCreateSubscription.mockRejectedValue(new Error('Lỗi test')); render( , ); await userEvent.click(screen.getByRole('button', { name: /thanh toán/i })); await waitFor(() => { expect(screen.getByText('Lỗi test')).toBeInTheDocument(); }); await userEvent.click(screen.getByText('Đóng')); expect(screen.queryByText('Lỗi test')).not.toBeInTheDocument(); }); });