fix(tip): allow checkout for all auth states
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { act } from "react";
|
||||
import { createRoot, type Root } from "react-dom/client";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import {
|
||||
PaymentPlanSchema,
|
||||
type PaymentPlan,
|
||||
} from "@/data/schemas/payment";
|
||||
import type { PaymentContextState } from "@/stores/payment/payment-context";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
payment: null as unknown as PaymentContextState,
|
||||
paymentDispatch: vi.fn(),
|
||||
planClick: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/app/_components", () => ({
|
||||
BackButton: () => null,
|
||||
CharacterAvatar: () => null,
|
||||
}));
|
||||
vi.mock("@/app/_components/core", () => ({
|
||||
MobileShell: ({ children }: { children: React.ReactNode }) => children,
|
||||
}));
|
||||
vi.mock("@/app/_components/payment/payment-method-selector", () => ({
|
||||
PaymentMethodSelector: () => null,
|
||||
}));
|
||||
vi.mock("@/app/_hooks/use-payment-method-selection", () => ({
|
||||
usePaymentMethodSelection: () => undefined,
|
||||
}));
|
||||
vi.mock("@/app/_hooks/use-payment-plan-analytics", () => ({
|
||||
usePaymentPlanAnalytics: () => undefined,
|
||||
}));
|
||||
vi.mock("@/app/_hooks/use-payment-route-flow", () => ({
|
||||
usePaymentRouteFlow: () => ({
|
||||
payment: mocks.payment,
|
||||
paymentDispatch: mocks.paymentDispatch,
|
||||
}),
|
||||
}));
|
||||
vi.mock("@/lib/analytics", () => ({
|
||||
behaviorAnalytics: { planClick: mocks.planClick },
|
||||
}));
|
||||
vi.mock("@/providers/character-provider", () => ({
|
||||
useActiveCharacter: () => ({
|
||||
id: "maya-tan",
|
||||
displayName: "Maya Tan",
|
||||
assets: {
|
||||
avatar: "/images/avatar/maya.png",
|
||||
cover: "/images/cover/maya.png",
|
||||
},
|
||||
copy: {
|
||||
tipHeader: "Tip Maya",
|
||||
tipTitle: "Buy Maya a coffee",
|
||||
},
|
||||
}),
|
||||
useActiveCharacterRoutes: () => ({
|
||||
splash: "/characters/maya/splash",
|
||||
tip: "/characters/maya/tip",
|
||||
}),
|
||||
}));
|
||||
vi.mock("@/stores/user/user-context", () => ({
|
||||
useUserState: () => ({ currentUser: null }),
|
||||
}));
|
||||
vi.mock("../tip-checkout-button", () => ({
|
||||
TipCheckoutButton: ({
|
||||
disabled,
|
||||
onOrder,
|
||||
}: {
|
||||
disabled?: boolean;
|
||||
onOrder: () => void;
|
||||
}) => (
|
||||
<button
|
||||
type="button"
|
||||
data-testid="tip-checkout"
|
||||
disabled={disabled}
|
||||
onClick={onOrder}
|
||||
>
|
||||
Checkout
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
vi.mock("../tip-coffee-tier-selector", () => ({
|
||||
TipCoffeeTierSelector: () => null,
|
||||
}));
|
||||
|
||||
import { TipScreen } from "../tip-screen";
|
||||
|
||||
const mediumPlan: PaymentPlan = PaymentPlanSchema.parse({
|
||||
planId: "tip_coffee_usd_9_99",
|
||||
planName: "Gilded Heart",
|
||||
orderType: "tip",
|
||||
vipDays: null,
|
||||
dolAmount: null,
|
||||
creditBalance: 0,
|
||||
amountCents: 999,
|
||||
originalAmountCents: null,
|
||||
dailyPriceCents: null,
|
||||
currency: "USD",
|
||||
isFirstRechargeOffer: false,
|
||||
mostPopular: false,
|
||||
firstRechargeDiscountPercent: null,
|
||||
promotionType: null,
|
||||
});
|
||||
|
||||
describe("TipScreen checkout", () => {
|
||||
let container: HTMLDivElement;
|
||||
let root: Root;
|
||||
|
||||
beforeEach(() => {
|
||||
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean })
|
||||
.IS_REACT_ACT_ENVIRONMENT = true;
|
||||
mocks.payment = makePaymentState();
|
||||
mocks.paymentDispatch.mockReset();
|
||||
mocks.planClick.mockReset();
|
||||
container = document.createElement("div");
|
||||
document.body.append(container);
|
||||
root = createRoot(container);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
act(() => root.unmount());
|
||||
container.remove();
|
||||
});
|
||||
|
||||
it("creates a character-attributed order without an AuthProvider", () => {
|
||||
renderScreen();
|
||||
const checkout = getCheckoutButton();
|
||||
|
||||
expect(checkout.disabled).toBe(false);
|
||||
act(() => checkout.click());
|
||||
|
||||
expect(mocks.planClick).toHaveBeenCalledWith(
|
||||
mediumPlan,
|
||||
expect.objectContaining({ entryPoint: "tip_page" }),
|
||||
);
|
||||
expect(mocks.paymentDispatch).toHaveBeenCalledWith({
|
||||
type: "PaymentCreateOrderSubmitted",
|
||||
recipientCharacterId: "maya-tan",
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
["plans are loading", { isLoadingPlans: true }],
|
||||
["the selected plan is missing", { plans: [], selectedPlanId: "" }],
|
||||
["an order is being created", { isCreatingOrder: true }],
|
||||
["an order is being polled", { isPollingOrder: true }],
|
||||
["the order is paid", { isPaid: true }],
|
||||
] as const)("disables checkout when %s", (_label, overrides) => {
|
||||
mocks.payment = makePaymentState(overrides);
|
||||
renderScreen();
|
||||
|
||||
expect(getCheckoutButton().disabled).toBe(true);
|
||||
});
|
||||
|
||||
function renderScreen(): void {
|
||||
act(() => root.render(<TipScreen />));
|
||||
}
|
||||
|
||||
function getCheckoutButton(): HTMLButtonElement {
|
||||
const button = container.querySelector<HTMLButtonElement>(
|
||||
'[data-testid="tip-checkout"]',
|
||||
);
|
||||
if (!button) throw new Error("Missing Tip checkout button");
|
||||
return button;
|
||||
}
|
||||
});
|
||||
|
||||
function makePaymentState(
|
||||
overrides: Partial<PaymentContextState> = {},
|
||||
): PaymentContextState {
|
||||
return {
|
||||
status: "ready",
|
||||
plans: [mediumPlan],
|
||||
isFirstRecharge: false,
|
||||
selectedPlanId: mediumPlan.planId,
|
||||
payChannel: "stripe",
|
||||
autoRenew: false,
|
||||
agreed: true,
|
||||
currentOrderId: null,
|
||||
payParams: null,
|
||||
orderStatus: null,
|
||||
errorMessage: null,
|
||||
launchNonce: 0,
|
||||
isLoadingPlans: false,
|
||||
isCreatingOrder: false,
|
||||
isPollingOrder: false,
|
||||
isPaid: false,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user