import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, beforeEach, describe, expect, it, test, vi } from "vitest";
import OpenHands from "#/api/open-hands";
import { PaymentForm } from "#/components/features/payment/payment-form";
describe("PaymentForm", () => {
const getBalanceSpy = vi.spyOn(OpenHands, "getBalance");
const createCheckoutSessionSpy = vi.spyOn(OpenHands, "createCheckoutSession");
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
const renderPaymentForm = () =>
render(, {
wrapper: ({ children }) => (
{children}
),
});
beforeEach(() => {
// useBalance hook will return the balance only if the APP_MODE is "saas" and the billing feature is enabled
getConfigSpy.mockResolvedValue({
APP_MODE: "saas",
GITHUB_CLIENT_ID: "123",
POSTHOG_CLIENT_KEY: "456",
FEATURE_FLAGS: {
ENABLE_BILLING: true,
HIDE_LLM_SETTINGS: false,
},
});
});
afterEach(() => {
vi.clearAllMocks();
});
it("should render the users current balance", async () => {
getBalanceSpy.mockResolvedValue("100.50");
renderPaymentForm();
await waitFor(() => {
const balance = screen.getByTestId("user-balance");
expect(balance).toHaveTextContent("$100.50");
});
});
it("should render the users current balance to two decimal places", async () => {
getBalanceSpy.mockResolvedValue("100");
renderPaymentForm();
await waitFor(() => {
const balance = screen.getByTestId("user-balance");
expect(balance).toHaveTextContent("$100.00");
});
});
test("the user can top-up a specific amount", async () => {
const user = userEvent.setup();
renderPaymentForm();
const topUpInput = await screen.findByTestId("top-up-input");
await user.type(topUpInput, "50");
const topUpButton = screen.getByText("PAYMENT$ADD_CREDIT");
await user.click(topUpButton);
expect(createCheckoutSessionSpy).toHaveBeenCalledWith(50);
});
it("should only accept integer values", async () => {
const user = userEvent.setup();
renderPaymentForm();
const topUpInput = await screen.findByTestId("top-up-input");
await user.type(topUpInput, "50");
const topUpButton = screen.getByText("PAYMENT$ADD_CREDIT");
await user.click(topUpButton);
expect(createCheckoutSessionSpy).toHaveBeenCalledWith(50);
});
it("should disable the top-up button if the user enters an invalid amount", async () => {
const user = userEvent.setup();
renderPaymentForm();
const topUpButton = screen.getByText("PAYMENT$ADD_CREDIT");
expect(topUpButton).toBeDisabled();
const topUpInput = await screen.findByTestId("top-up-input");
await user.type(topUpInput, " ");
expect(topUpButton).toBeDisabled();
});
it("should disable the top-up button after submission", async () => {
const user = userEvent.setup();
renderPaymentForm();
const topUpInput = await screen.findByTestId("top-up-input");
await user.type(topUpInput, "50");
const topUpButton = screen.getByText("PAYMENT$ADD_CREDIT");
await user.click(topUpButton);
expect(topUpButton).toBeDisabled();
});
describe("prevent submission if", () => {
test("user enters a negative amount", async () => {
const user = userEvent.setup();
renderPaymentForm();
const topUpInput = await screen.findByTestId("top-up-input");
await user.type(topUpInput, "-50");
const topUpButton = screen.getByText("PAYMENT$ADD_CREDIT");
await user.click(topUpButton);
expect(createCheckoutSessionSpy).not.toHaveBeenCalled();
});
test("user enters an empty string", async () => {
const user = userEvent.setup();
renderPaymentForm();
const topUpInput = await screen.findByTestId("top-up-input");
await user.type(topUpInput, " ");
const topUpButton = screen.getByText("PAYMENT$ADD_CREDIT");
await user.click(topUpButton);
expect(createCheckoutSessionSpy).not.toHaveBeenCalled();
});
test("user enters a non-numeric value", async () => {
const user = userEvent.setup();
renderPaymentForm();
// With type="number", the browser would prevent non-numeric input,
// but we'll test the validation logic anyway
const topUpInput = await screen.findByTestId("top-up-input");
await user.type(topUpInput, "abc");
const topUpButton = screen.getByText("PAYMENT$ADD_CREDIT");
await user.click(topUpButton);
expect(createCheckoutSessionSpy).not.toHaveBeenCalled();
});
test("user enters less than the minimum amount", async () => {
const user = userEvent.setup();
renderPaymentForm();
const topUpInput = await screen.findByTestId("top-up-input");
await user.type(topUpInput, "9"); // test assumes the minimum is 10
const topUpButton = screen.getByText("PAYMENT$ADD_CREDIT");
await user.click(topUpButton);
expect(createCheckoutSessionSpy).not.toHaveBeenCalled();
});
test("user enters a decimal value", async () => {
const user = userEvent.setup();
renderPaymentForm();
// With step="1", the browser would validate this, but we'll test our validation logic
const topUpInput = await screen.findByTestId("top-up-input");
await user.type(topUpInput, "50.5");
const topUpButton = screen.getByText("PAYMENT$ADD_CREDIT");
await user.click(topUpButton);
expect(createCheckoutSessionSpy).not.toHaveBeenCalled();
});
});
});