Skip to content

Commit

Permalink
Merge commit '8d097efb4fdabeb9abd8be388315d49da5478b81' into xw/mem-leak
Browse files Browse the repository at this point in the history
  • Loading branch information
xingyaoww committed Feb 18, 2025
2 parents 4345611 + 8d097ef commit bc94fdd
Show file tree
Hide file tree
Showing 40 changed files with 3,626 additions and 2,782 deletions.
2 changes: 2 additions & 0 deletions docs/modules/usage/installation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
- Linux
- Windows with [WSL](https://learn.microsoft.com/en-us/windows/wsl/install) and [Docker Desktop support](https://docs.docker.com/desktop/setup/install/windows-install/#system-requirements)

A system with a modern processor and a minimum of **4GB RAM** is recommended to run OpenHands.

## Prerequisites

<details>
Expand Down
166 changes: 166 additions & 0 deletions frontend/__tests__/components/features/payment/payment-form.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
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(<PaymentForm />, {
wrapper: ({ children }) => (
<QueryClientProvider client={new QueryClient()}>
{children}
</QueryClientProvider>
),
});

beforeEach(() => {
// useBalance hook will return the balance only if the APP_MODE is "saas"
getConfigSpy.mockResolvedValue({
APP_MODE: "saas",
GITHUB_CLIENT_ID: "123",
POSTHOG_CLIENT_KEY: "456",
});
});

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.12");

const topUpButton = screen.getByText("Add credit");
await user.click(topUpButton);

expect(createCheckoutSessionSpy).toHaveBeenCalledWith(50.12);
});

it("should round the top-up amount to two decimal places", async () => {
const user = userEvent.setup();
renderPaymentForm();

const topUpInput = await screen.findByTestId("top-up-input");
await user.type(topUpInput, "50.125456");

const topUpButton = screen.getByText("Add credit");
await user.click(topUpButton);

expect(createCheckoutSessionSpy).toHaveBeenCalledWith(50.13);
});

it("should render the payment method link", async () => {
renderPaymentForm();

screen.getByTestId("payment-methods-link");
});

it("should disable the top-up button if the user enters an invalid amount", async () => {
const user = userEvent.setup();
renderPaymentForm();

const topUpButton = screen.getByText("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.12");

const topUpButton = screen.getByText("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.12");

const topUpButton = screen.getByText("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("Add credit");
await user.click(topUpButton);

expect(createCheckoutSessionSpy).not.toHaveBeenCalled();
});

test("user enters a non-numeric value", async () => {
const user = userEvent.setup();
renderPaymentForm();

const topUpInput = await screen.findByTestId("top-up-input");
await user.type(topUpInput, "abc");

const topUpButton = screen.getByText("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, "20"); // test assumes the minimum is 25

const topUpButton = screen.getByText("Add credit");
await user.click(topUpButton);

expect(createCheckoutSessionSpy).not.toHaveBeenCalled();
});
});
});
23 changes: 22 additions & 1 deletion frontend/__tests__/components/settings/settings-input.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { describe, expect, it, vi } from "vitest";
import userEvent from "@testing-library/user-event";
import { SettingsInput } from "#/components/features/settings/settings-input";

describe("SettingsInput", () => {
Expand Down Expand Up @@ -85,4 +86,24 @@ describe("SettingsInput", () => {

expect(screen.getByText("Start Content")).toBeInTheDocument();
});

it("should call onChange with the input value", async () => {
const onChangeMock = vi.fn();
const user = userEvent.setup();

render(
<SettingsInput
testId="test-input"
label="Test Input"
type="text"
onChange={onChangeMock}
/>,
);

const input = screen.getByTestId("test-input");
await user.type(input, "Test");

expect(onChangeMock).toHaveBeenCalledTimes(4);
expect(onChangeMock).toHaveBeenNthCalledWith(4, "Test");
});
});
17 changes: 10 additions & 7 deletions frontend/__tests__/routes/home.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,12 @@ describe("Home Screen", () => {
Component: Home,
path: "/",
},
{
Component: SettingsScreen,
path: "/settings",
},
],
},
{
Component: SettingsScreen,
path: "/settings",
},
]);

afterEach(() => {
Expand Down Expand Up @@ -96,6 +96,9 @@ describe("Home Screen", () => {
const user = userEvent.setup();
renderWithProviders(<RouterStub initialEntries={["/"]} />);

const settingsScreen = screen.queryByTestId("settings-screen");
expect(settingsScreen).not.toBeInTheDocument();

const settingsModal = await screen.findByTestId("ai-config-modal");
expect(settingsModal).toBeInTheDocument();

Expand All @@ -104,11 +107,11 @@ describe("Home Screen", () => {
);
await user.click(advancedSettingsButton);

const settingsScreenAfter = await screen.findByTestId("settings-screen");
expect(settingsScreenAfter).toBeInTheDocument();

const settingsModalAfter = screen.queryByTestId("ai-config-modal");
expect(settingsModalAfter).not.toBeInTheDocument();

const settingsScreen = await screen.findByTestId("settings-screen");
expect(settingsScreen).toBeInTheDocument();
});
});
});
83 changes: 83 additions & 0 deletions frontend/__tests__/routes/settings-with-payment.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { screen, waitFor, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, describe, expect, it, vi } from "vitest";
import { createRoutesStub } from "react-router";
import { renderWithProviders } from "test-utils";
import OpenHands from "#/api/open-hands";
import SettingsScreen from "#/routes/settings";
import { PaymentForm } from "#/components/features/payment/payment-form";
import * as FeatureFlags from "#/utils/feature-flags";

describe("Settings Billing", () => {
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
vi.spyOn(FeatureFlags, "BILLING_SETTINGS").mockReturnValue(true);

const RoutesStub = createRoutesStub([
{
Component: SettingsScreen,
path: "/settings",
children: [
{
Component: () => <PaymentForm />,
path: "/settings/billing",
},
],
},
]);

const renderSettingsScreen = () =>
renderWithProviders(<RoutesStub initialEntries={["/settings"]} />);

afterEach(() => {
vi.clearAllMocks();
});

it("should not render the navbar if OSS mode", async () => {
getConfigSpy.mockResolvedValue({
APP_MODE: "oss",
GITHUB_CLIENT_ID: "123",
POSTHOG_CLIENT_KEY: "456",
});

renderSettingsScreen();

await waitFor(() => {
const navbar = screen.queryByTestId("settings-navbar");
expect(navbar).not.toBeInTheDocument();
});
});

it("should render the navbar if SaaS mode", async () => {
getConfigSpy.mockResolvedValue({
APP_MODE: "saas",
GITHUB_CLIENT_ID: "123",
POSTHOG_CLIENT_KEY: "456",
});

renderSettingsScreen();

await waitFor(() => {
const navbar = screen.getByTestId("settings-navbar");
within(navbar).getByText("Account");
within(navbar).getByText("Credits");
});
});

it("should render the billing settings if clicking the credits item", async () => {
const user = userEvent.setup();
getConfigSpy.mockResolvedValue({
APP_MODE: "saas",
GITHUB_CLIENT_ID: "123",
POSTHOG_CLIENT_KEY: "456",
});

renderSettingsScreen();

const navbar = await screen.findByTestId("settings-navbar");
const credits = within(navbar).getByText("Credits");
await user.click(credits);

const billingSection = await screen.findByTestId("billing-settings");
within(billingSection).getByText("Manage Credits");
});
});
Loading

0 comments on commit bc94fdd

Please sign in to comment.