feat(tip): refine single-screen payment layout

This commit is contained in:
2026-07-21 15:15:50 +08:00
parent fa08cc114f
commit 612c4139af
5 changed files with 509 additions and 436 deletions
@@ -20,14 +20,18 @@ const mocks = vi.hoisted(() => ({
}));
vi.mock("@/app/_components", () => ({
BackButton: () => null,
CharacterAvatar: () => null,
BackButton: () => <span data-testid="tip-back-button" />,
CharacterAvatar: ({ alt }: { alt: string }) => (
<span data-testid="tip-character-avatar">{alt}</span>
),
}));
vi.mock("@/app/_components/core", () => ({
MobileShell: ({ children }: { children: React.ReactNode }) => children,
}));
vi.mock("@/app/_components/payment/payment-method-selector", () => ({
PaymentMethodSelector: () => null,
PaymentMethodSelector: ({ density }: { density?: string }) => (
<span data-testid="tip-payment-method" data-density={density} />
),
}));
vi.mock("@/app/_hooks/use-payment-method-selection", () => ({
usePaymentMethodSelection: () => undefined,
@@ -85,10 +89,20 @@ vi.mock("../tip-checkout-button", () => ({
),
}));
vi.mock("../tip-gift-product-selector", () => ({
TipGiftProductSelector: () => null,
TipGiftProductSelector: ({
products,
}: {
products: readonly { planId: string; planName: string }[];
}) => (
<span data-testid="tip-product-selector">
{products.map((product) => product.planName).join(",")}
</span>
),
}));
vi.mock("../tip-product-image", () => ({
TipProductImage: ({ alt }: { alt: string }) => <span>{alt}</span>,
TipProductImage: ({ alt }: { alt: string }) => (
<span data-testid="tip-product-image">{alt}</span>
),
}));
vi.mock("../use-tip-support-prompt", () => ({
useTipSupportPrompt: () => ({
@@ -174,12 +188,30 @@ describe("TipScreen checkout", () => {
});
});
it("renders the compact purchase flow with character atmosphere", () => {
renderScreen();
expect(container.textContent).toContain("Send Maya a gift");
expect(container.textContent).toContain("A warm coffee prompt.");
expect(container.querySelector('[data-testid="tip-back-button"]')).not.toBeNull();
expect(container.querySelector('[data-testid="tip-character-avatar"]')).not.toBeNull();
expect(container.querySelector('[data-testid="tip-product-image"]')).not.toBeNull();
expect(container.querySelector('[data-testid="tip-product-selector"]')).not.toBeNull();
expect(
container
.querySelector('[data-testid="tip-payment-method"]')
?.getAttribute("data-density"),
).toBe("compact");
expect(container.textContent).not.toContain("A little sweetness for today");
expect(container.textContent).not.toContain("Tip Maya");
expect(
container
.querySelector("main")
?.getAttribute("style"),
).toContain('--tip-cover-image: url("/images/cover/maya.png")');
});
it.each([
["catalog is loading", { isLoadingPlans: true }],
[
"the selected product is missing",
{ plans: [], giftProducts: [], selectedPlanId: "" },
],
["an order is being created", { isCreatingOrder: true }],
["an order is being polled", { isPollingOrder: true }],
] as const)("disables checkout when %s", (_label, overrides) => {
@@ -188,6 +220,38 @@ describe("TipScreen checkout", () => {
expect(getCheckoutButton().disabled).toBe(true);
});
it.each([
["catalog is loading", { isLoadingPlans: true }],
[
"the catalog is empty",
{ plans: [], giftProducts: [], selectedPlanId: "" },
],
] as const)("hides payment controls when %s", (_label, overrides) => {
mocks.payment = makePaymentState(overrides);
renderScreen();
expect(container.querySelector('[data-testid="tip-checkout"]')).toBeNull();
expect(container.querySelector('[data-testid="tip-payment-method"]')).toBeNull();
});
it("shows a compact retry state when the catalog fails", () => {
mocks.payment = makePaymentState({
plans: [],
giftProducts: [],
selectedPlanId: "",
errorMessage: "catalog unavailable",
});
renderScreen();
expect(container.textContent).toContain(
"We could not load gifts for this character.",
);
act(() => getButton("Try again").click());
expect(mocks.paymentDispatch).toHaveBeenCalledWith({
type: "PaymentCatalogRetryRequested",
});
});
it("shows fixed copy for the first character Tip", () => {
mocks.payment = makePaymentState({
status: "paid",