feat(subscription): separate selection from payment and add issue feedback
(cherry picked from commit fe9d31146b)
This commit is contained in:
@@ -0,0 +1,224 @@
|
||||
import { act } from "react";
|
||||
import { createRoot, type Root } from "react-dom/client";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { Result } from "@/utils/result";
|
||||
|
||||
import { SubscriptionPaymentIssueDialog } from "../subscription-payment-issue-dialog";
|
||||
import { SubscriptionRenewalConfirmationDialog } from "../subscription-renewal-confirmation-dialog";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
submitFeedback: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/feedback", () => ({
|
||||
submitFeedback: mocks.submitFeedback,
|
||||
}));
|
||||
|
||||
vi.mock("@/app/feedback/feedback-context", () => ({
|
||||
createFeedbackContext: () => ({
|
||||
appVersion: "test",
|
||||
platform: "web",
|
||||
browser: "Chrome",
|
||||
viewport: "390x844@3",
|
||||
}),
|
||||
}));
|
||||
|
||||
describe("subscription payment dialogs", () => {
|
||||
let container: HTMLDivElement;
|
||||
let root: Root;
|
||||
|
||||
beforeEach(() => {
|
||||
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean })
|
||||
.IS_REACT_ACT_ENVIRONMENT = true;
|
||||
mocks.submitFeedback.mockReset();
|
||||
container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
act(() => root.unmount());
|
||||
container.remove();
|
||||
document.body.querySelectorAll('[role="dialog"]').forEach((node) => node.remove());
|
||||
});
|
||||
|
||||
it("confirms a VIP plan without submitting payment", () => {
|
||||
const onConfirm = vi.fn();
|
||||
const onCancel = vi.fn();
|
||||
act(() =>
|
||||
root.render(
|
||||
<SubscriptionRenewalConfirmationDialog
|
||||
open
|
||||
plan={{
|
||||
id: "vip_monthly",
|
||||
title: "Monthly",
|
||||
price: "19.90",
|
||||
currency: "usd",
|
||||
originalPrice: "39.90",
|
||||
}}
|
||||
onCancel={onCancel}
|
||||
onConfirm={onConfirm}
|
||||
/>,
|
||||
),
|
||||
);
|
||||
|
||||
const dialog = document.body.querySelector('[role="dialog"]');
|
||||
expect(dialog?.textContent).toContain("Automatic Renewal Confirmation");
|
||||
expect(dialog?.textContent).toContain("Monthly");
|
||||
expect(dialog?.textContent).toContain("19.90 usd");
|
||||
expect(dialog?.querySelectorAll("a")).toHaveLength(2);
|
||||
|
||||
act(() => clickButton(dialog, "Confirm"));
|
||||
expect(onConfirm).toHaveBeenCalledOnce();
|
||||
expect(onCancel).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("submits Other as a Manager payment issue with one description field", async () => {
|
||||
mocks.submitFeedback.mockResolvedValue(
|
||||
Result.ok({ feedbackId: "feedback-payment-1" }),
|
||||
);
|
||||
const onClose = vi.fn();
|
||||
const onSubmitted = vi.fn();
|
||||
|
||||
act(() =>
|
||||
root.render(
|
||||
<SubscriptionPaymentIssueDialog
|
||||
open
|
||||
subscriptionType="vip"
|
||||
planId="vip_monthly"
|
||||
orderId={null}
|
||||
payChannel="ezpay"
|
||||
countryCode="ID"
|
||||
characterId="maya"
|
||||
onClose={onClose}
|
||||
onSubmitted={onSubmitted}
|
||||
/>,
|
||||
),
|
||||
);
|
||||
|
||||
const other = document.body.querySelector<HTMLInputElement>(
|
||||
'input[value="other"]',
|
||||
);
|
||||
expect(
|
||||
Array.from(document.body.querySelectorAll<HTMLInputElement>('input[type="radio"]')).map(
|
||||
(input) => input.parentElement?.textContent?.trim(),
|
||||
),
|
||||
).toEqual([
|
||||
"Concerned about card information security",
|
||||
"Insufficient card or wallet balance",
|
||||
"No supported payment method available",
|
||||
"Too expensive",
|
||||
"Other",
|
||||
]);
|
||||
act(() => {
|
||||
other?.click();
|
||||
});
|
||||
const textarea = document.body.querySelector("textarea");
|
||||
expect(textarea?.getAttribute("placeholder")).toBe(
|
||||
"Please describe the payment problem you encountered.",
|
||||
);
|
||||
act(() => setTextareaValue(textarea, "QRIS did not open correctly."));
|
||||
|
||||
await act(async () => {
|
||||
clickButton(document.body, "Submit");
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(mocks.submitFeedback).toHaveBeenCalledWith({
|
||||
category: "payment",
|
||||
content: "Payment issue: Other\nDetails: QRIS did not open correctly.",
|
||||
context: {
|
||||
appVersion: "test",
|
||||
platform: "web",
|
||||
browser: "Chrome",
|
||||
viewport: "390x844@3",
|
||||
sourcePage: "subscription",
|
||||
paymentIssueReason: "other",
|
||||
subscriptionType: "vip",
|
||||
planId: "vip_monthly",
|
||||
payChannel: "ezpay",
|
||||
countryCode: "ID",
|
||||
characterId: "maya",
|
||||
},
|
||||
images: [],
|
||||
idempotencyKey: expect.stringMatching(/^payment_feedback_[A-Za-z0-9_-]+$/),
|
||||
});
|
||||
expect(onSubmitted).toHaveBeenCalledWith(
|
||||
"Thanks. Your payment issue has been submitted.",
|
||||
);
|
||||
expect(onClose).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("keeps a failed payment issue open and reuses its idempotency key for retry", async () => {
|
||||
mocks.submitFeedback
|
||||
.mockResolvedValueOnce(Result.err(new Error("backend unavailable")))
|
||||
.mockResolvedValueOnce(Result.ok({ feedbackId: "feedback-payment-2" }));
|
||||
const onClose = vi.fn();
|
||||
|
||||
act(() =>
|
||||
root.render(
|
||||
<SubscriptionPaymentIssueDialog
|
||||
open
|
||||
subscriptionType="topup"
|
||||
planId="coin_1000"
|
||||
orderId="pay_pending_1"
|
||||
payChannel="stripe"
|
||||
countryCode="US"
|
||||
characterId="elio"
|
||||
onClose={onClose}
|
||||
onSubmitted={() => undefined}
|
||||
/>,
|
||||
),
|
||||
);
|
||||
|
||||
const reason = document.body.querySelector<HTMLInputElement>(
|
||||
'input[value="tooExpensive"]',
|
||||
);
|
||||
act(() => reason?.click());
|
||||
await act(async () => {
|
||||
clickButton(document.body, "Submit");
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(document.body.textContent).toContain(
|
||||
"Unable to submit. Please try again.",
|
||||
);
|
||||
expect(onClose).not.toHaveBeenCalled();
|
||||
|
||||
await act(async () => {
|
||||
clickButton(document.body, "Submit");
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(mocks.submitFeedback).toHaveBeenCalledTimes(2);
|
||||
expect(mocks.submitFeedback.mock.calls[0]?.[0].idempotencyKey).toBe(
|
||||
mocks.submitFeedback.mock.calls[1]?.[0].idempotencyKey,
|
||||
);
|
||||
expect(onClose).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
|
||||
function clickButton(root: ParentNode | null, label: string): void {
|
||||
const button = Array.from(root?.querySelectorAll("button") ?? []).find(
|
||||
(item) => item.textContent?.trim() === label,
|
||||
);
|
||||
if (!button) throw new Error(`Expected ${label} button`);
|
||||
button.click();
|
||||
}
|
||||
|
||||
function setTextareaValue(
|
||||
element: HTMLTextAreaElement | null,
|
||||
value: string,
|
||||
): void {
|
||||
if (!element) throw new Error("Expected textarea");
|
||||
const setter = Object.getOwnPropertyDescriptor(
|
||||
HTMLTextAreaElement.prototype,
|
||||
"value",
|
||||
)?.set;
|
||||
setter?.call(element, value);
|
||||
element.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
}
|
||||
Reference in New Issue
Block a user