import { act } from "react"; import { createRoot, type Root } from "react-dom/client"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; const mocks = vi.hoisted(() => ({ dispatch: vi.fn(), handleQrisResume: vi.fn(), hasHiddenQrisPayment: false, resetLaunchState: vi.fn(), payment: { selectedPlanId: "vip_monthly", autoRenew: true, payChannel: "stripe" as "stripe" | "ezpay", commercialOfferId: null, chatActionId: null, currentOrderId: null, errorMessage: null, isCreatingOrder: false, isPollingOrder: false, }, })); vi.mock("@/stores/payment/payment-context", () => ({ usePaymentState: () => mocks.payment, usePaymentDispatch: () => mocks.dispatch, })); vi.mock("@/app/_hooks/use-payment-launch-flow", () => ({ usePaymentLaunchFlow: () => ({ handleQrisResume: mocks.handleQrisResume, hasHiddenQrisPayment: mocks.hasHiddenQrisPayment, resetLaunchState: mocks.resetLaunchState, launch: {}, }), })); vi.mock("@/app/_components/payment/payment-launch-dialogs", () => ({ PaymentLaunchDialogs: () => null, })); vi.mock("@/app/_components/payment/external-browser-checkout-button", () => ({ ExternalBrowserCheckoutButton: () => null, })); vi.mock("../subscription-cta-button", () => ({ SubscriptionCtaButton: ({ children, disabled, onClick, }: { children: React.ReactNode; disabled?: boolean; onClick?: () => void; }) => ( ), })); vi.mock("../subscription-renewal-confirmation-dialog", () => ({ SubscriptionRenewalConfirmationDialog: ({ open, onCancel, onConfirm, }: { open: boolean; onCancel: () => void; onConfirm: () => void; }) => open ? (
) : null, })); import { SubscriptionCheckoutButton } from "../subscription-checkout-button"; const renewalPlan = { id: "vip_monthly", title: "Monthly", price: "19.90", currency: "usd", originalPrice: "39.90", }; describe("SubscriptionCheckoutButton renewal confirmation", () => { let container: HTMLDivElement; let root: Root; beforeEach(() => { (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }) .IS_REACT_ACT_ENVIRONMENT = true; localStorage.clear(); mocks.dispatch.mockClear(); mocks.handleQrisResume.mockClear(); mocks.hasHiddenQrisPayment = false; mocks.resetLaunchState.mockClear(); mocks.payment.selectedPlanId = "vip_monthly"; mocks.payment.autoRenew = true; mocks.payment.payChannel = "stripe"; mocks.payment.isCreatingOrder = false; mocks.payment.isPollingOrder = false; container = document.createElement("div"); document.body.appendChild(container); root = createRoot(container); }); afterEach(() => { act(() => root.unmount()); container.remove(); }); it("shows confirmation on the first auto-renewing VIP checkout and resumes after confirm", () => { renderButton(root, "user-1"); act(() => clickButton(container, "Pay and Top Up")); expect(container.querySelector('[role="dialog"]')).not.toBeNull(); expect(mocks.dispatch).not.toHaveBeenCalled(); act(() => clickButton(container, "Confirm")); expect(container.querySelector('[role="dialog"]')).toBeNull(); expect(mocks.resetLaunchState).toHaveBeenCalledOnce(); expect(mocks.dispatch).toHaveBeenCalledWith({ type: "PaymentCreateOrderSubmitted", }); }); it("does not ask the same user again after confirmation", () => { renderButton(root, "user-1"); act(() => clickButton(container, "Pay and Top Up")); act(() => clickButton(container, "Confirm")); mocks.dispatch.mockClear(); mocks.resetLaunchState.mockClear(); act(() => root.unmount()); root = createRoot(container); renderButton(root, "user-1"); act(() => clickButton(container, "Pay and Top Up")); expect(container.querySelector('[role="dialog"]')).toBeNull(); expect(mocks.resetLaunchState).toHaveBeenCalledOnce(); expect(mocks.dispatch).toHaveBeenCalledWith({ type: "PaymentCreateOrderSubmitted", }); }); it("keeps asking after cancel and isolates acknowledgement by user", () => { renderButton(root, "user-1"); act(() => clickButton(container, "Pay and Top Up")); act(() => clickButton(container, "Cancel")); act(() => clickButton(container, "Pay and Top Up")); expect(container.querySelector('[role="dialog"]')).not.toBeNull(); act(() => clickButton(container, "Confirm")); mocks.dispatch.mockClear(); act(() => root.unmount()); root = createRoot(container); renderButton(root, "user-2"); act(() => clickButton(container, "Pay and Top Up")); expect(container.querySelector('[role="dialog"]')).not.toBeNull(); expect(mocks.dispatch).not.toHaveBeenCalled(); }); it("skips confirmation for a non-renewing purchase", () => { mocks.payment.autoRenew = false; renderButton(root, "user-1", null); act(() => clickButton(container, "Pay and Top Up")); expect(container.querySelector('[role="dialog"]')).toBeNull(); expect(mocks.dispatch).toHaveBeenCalledWith({ type: "PaymentCreateOrderSubmitted", }); }); it("skips confirmation for one-time EzPay membership checkout", () => { mocks.payment.payChannel = "ezpay"; renderButton(root, "user-1"); act(() => clickButton(container, "Pay and Top Up")); expect(container.querySelector('[role="dialog"]')).toBeNull(); expect(mocks.dispatch).toHaveBeenCalledWith({ type: "PaymentCreateOrderSubmitted", }); }); it("reopens a hidden QRIS order without creating another order", () => { mocks.payment.payChannel = "ezpay"; mocks.payment.isPollingOrder = true; mocks.hasHiddenQrisPayment = true; act(() => root.render( , ), ); const resumeButton = getButton(container, "Resume QRIS payment"); expect(resumeButton.disabled).toBe(false); act(() => resumeButton.click()); expect(mocks.handleQrisResume).toHaveBeenCalledOnce(); expect(mocks.resetLaunchState).not.toHaveBeenCalled(); expect(mocks.dispatch).not.toHaveBeenCalled(); }); }); function renderButton( root: Root, renewalConsentSubjectId: string, plan: typeof renewalPlan | null = renewalPlan, ): void { act(() => root.render( , ), ); } function clickButton(root: ParentNode, label: string): void { getButton(root, label).click(); } function getButton(root: ParentNode, label: string): HTMLButtonElement { const button = Array.from(root.querySelectorAll("button")).find( (item) => item.textContent?.trim() === label, ); if (!button) throw new Error(`Expected ${label} button`); return button; }