mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-23 06:25:04 +00:00
test: add comprehensive Vitest coverage for CostTrackingSettings
Add 88 tests across 9 test files for the CostTrackingSettings component directory: - provider_display_helpers.test.ts: 9 tests for helper functions - how_it_works.test.tsx: 9 tests for discount calculator component - add_provider_form.test.tsx: 7 tests for provider form validation - add_margin_form.test.tsx: 9 tests for margin form with type toggle - provider_discount_table.test.tsx: 12 tests for table editing and interactions - provider_margin_table.test.tsx: 13 tests for margin table with sorting - use_discount_config.test.ts: 11 tests for discount hook logic - use_margin_config.test.ts: 12 tests for margin hook logic - cost_tracking_settings.test.tsx: 15 tests for main component and role-based rendering All tests passing. Coverage includes form validation, user interactions, API calls, state management, and conditional rendering. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Haiku 4.5
parent
332dc32299
commit
c4a0174e00
@@ -0,0 +1,148 @@
|
||||
import React from "react";
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { renderWithProviders } from "../../../tests/test-utils";
|
||||
import AddMarginForm from "./add_margin_form";
|
||||
import { MarginConfig } from "./types";
|
||||
|
||||
vi.mock("../provider_info_helpers", () => ({
|
||||
Providers: {
|
||||
OpenAI: "OpenAI",
|
||||
Anthropic: "Anthropic",
|
||||
},
|
||||
provider_map: {
|
||||
OpenAI: "openai",
|
||||
Anthropic: "anthropic",
|
||||
},
|
||||
providerLogoMap: {
|
||||
OpenAI: "https://example.com/openai.png",
|
||||
Anthropic: "https://example.com/anthropic.png",
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("./provider_display_helpers", () => ({
|
||||
handleImageError: vi.fn(),
|
||||
}));
|
||||
|
||||
const DEFAULT_PROPS = {
|
||||
marginConfig: {} as MarginConfig,
|
||||
selectedProvider: undefined,
|
||||
marginType: "percentage" as const,
|
||||
percentageValue: "",
|
||||
fixedAmountValue: "",
|
||||
onProviderChange: vi.fn(),
|
||||
onMarginTypeChange: vi.fn(),
|
||||
onPercentageChange: vi.fn(),
|
||||
onFixedAmountChange: vi.fn(),
|
||||
onAddProvider: vi.fn(),
|
||||
};
|
||||
|
||||
describe("AddMarginForm", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should render", () => {
|
||||
renderWithProviders(<AddMarginForm {...DEFAULT_PROPS} />);
|
||||
expect(screen.getByRole("button", { name: /add provider margin/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show the percentage input when marginType is percentage", () => {
|
||||
renderWithProviders(<AddMarginForm {...DEFAULT_PROPS} marginType="percentage" />);
|
||||
expect(screen.getByPlaceholderText("10")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show the fixed amount input when marginType is fixed", () => {
|
||||
renderWithProviders(<AddMarginForm {...DEFAULT_PROPS} marginType="fixed" />);
|
||||
expect(screen.getByPlaceholderText("0.001")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should not show the fixed amount input when marginType is percentage", () => {
|
||||
renderWithProviders(<AddMarginForm {...DEFAULT_PROPS} marginType="percentage" />);
|
||||
expect(screen.queryByPlaceholderText("0.001")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should not show the percentage input when marginType is fixed", () => {
|
||||
renderWithProviders(<AddMarginForm {...DEFAULT_PROPS} marginType="fixed" />);
|
||||
expect(screen.queryByPlaceholderText("10")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show the Percentage-based and Fixed Amount radio options", () => {
|
||||
renderWithProviders(<AddMarginForm {...DEFAULT_PROPS} />);
|
||||
expect(screen.getByText("Percentage-based")).toBeInTheDocument();
|
||||
expect(screen.getByText("Fixed Amount")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should disable the submit button when no provider is selected (percentage mode)", () => {
|
||||
renderWithProviders(
|
||||
<AddMarginForm {...DEFAULT_PROPS} selectedProvider={undefined} percentageValue="10" />
|
||||
);
|
||||
expect(screen.getByRole("button", { name: /add provider margin/i })).toBeDisabled();
|
||||
});
|
||||
|
||||
it("should disable the submit button when provider is selected but no percentage value (percentage mode)", () => {
|
||||
renderWithProviders(
|
||||
<AddMarginForm {...DEFAULT_PROPS} selectedProvider="OpenAI" percentageValue="" />
|
||||
);
|
||||
expect(screen.getByRole("button", { name: /add provider margin/i })).toBeDisabled();
|
||||
});
|
||||
|
||||
it("should enable the submit button when provider and percentage value are both provided", () => {
|
||||
renderWithProviders(
|
||||
<AddMarginForm {...DEFAULT_PROPS} selectedProvider="OpenAI" percentageValue="10" />
|
||||
);
|
||||
expect(screen.getByRole("button", { name: /add provider margin/i })).not.toBeDisabled();
|
||||
});
|
||||
|
||||
it("should disable the submit button in fixed mode when no fixed amount is provided", () => {
|
||||
renderWithProviders(
|
||||
<AddMarginForm
|
||||
{...DEFAULT_PROPS}
|
||||
selectedProvider="OpenAI"
|
||||
marginType="fixed"
|
||||
fixedAmountValue=""
|
||||
/>
|
||||
);
|
||||
expect(screen.getByRole("button", { name: /add provider margin/i })).toBeDisabled();
|
||||
});
|
||||
|
||||
it("should enable the submit button in fixed mode when provider and fixed amount are provided", () => {
|
||||
renderWithProviders(
|
||||
<AddMarginForm
|
||||
{...DEFAULT_PROPS}
|
||||
selectedProvider="OpenAI"
|
||||
marginType="fixed"
|
||||
fixedAmountValue="0.001"
|
||||
/>
|
||||
);
|
||||
expect(screen.getByRole("button", { name: /add provider margin/i })).not.toBeDisabled();
|
||||
});
|
||||
|
||||
it("should call onAddProvider when the enabled submit button is clicked", async () => {
|
||||
const onAddProvider = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(
|
||||
<AddMarginForm
|
||||
{...DEFAULT_PROPS}
|
||||
selectedProvider="OpenAI"
|
||||
percentageValue="10"
|
||||
onAddProvider={onAddProvider}
|
||||
/>
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /add provider margin/i }));
|
||||
expect(onAddProvider).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should call onMarginTypeChange when the Fixed Amount radio is clicked", async () => {
|
||||
const onMarginTypeChange = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(
|
||||
<AddMarginForm {...DEFAULT_PROPS} onMarginTypeChange={onMarginTypeChange} />
|
||||
);
|
||||
|
||||
await user.click(screen.getByText("Fixed Amount"));
|
||||
expect(onMarginTypeChange).toHaveBeenCalledWith("fixed");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,98 @@
|
||||
import React from "react";
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { renderWithProviders } from "../../../tests/test-utils";
|
||||
import AddProviderForm from "./add_provider_form";
|
||||
import { DiscountConfig } from "./types";
|
||||
|
||||
vi.mock("../provider_info_helpers", () => ({
|
||||
Providers: {
|
||||
OpenAI: "OpenAI",
|
||||
Anthropic: "Anthropic",
|
||||
},
|
||||
provider_map: {
|
||||
OpenAI: "openai",
|
||||
Anthropic: "anthropic",
|
||||
},
|
||||
providerLogoMap: {
|
||||
OpenAI: "https://example.com/openai.png",
|
||||
Anthropic: "https://example.com/anthropic.png",
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("./provider_display_helpers", () => ({
|
||||
handleImageError: vi.fn(),
|
||||
}));
|
||||
|
||||
const DEFAULT_PROPS = {
|
||||
discountConfig: {} as DiscountConfig,
|
||||
selectedProvider: undefined,
|
||||
newDiscount: "",
|
||||
onProviderChange: vi.fn(),
|
||||
onDiscountChange: vi.fn(),
|
||||
onAddProvider: vi.fn(),
|
||||
};
|
||||
|
||||
describe("AddProviderForm", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should render", () => {
|
||||
renderWithProviders(<AddProviderForm {...DEFAULT_PROPS} />);
|
||||
expect(screen.getByRole("button", { name: /add provider discount/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render the discount percentage input field", () => {
|
||||
renderWithProviders(<AddProviderForm {...DEFAULT_PROPS} />);
|
||||
expect(screen.getByPlaceholderText("5")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should disable the submit button when no provider is selected and no discount is entered", () => {
|
||||
renderWithProviders(<AddProviderForm {...DEFAULT_PROPS} />);
|
||||
expect(screen.getByRole("button", { name: /add provider discount/i })).toBeDisabled();
|
||||
});
|
||||
|
||||
it("should disable the submit button when a provider is selected but no discount is entered", () => {
|
||||
renderWithProviders(
|
||||
<AddProviderForm {...DEFAULT_PROPS} selectedProvider="OpenAI" newDiscount="" />
|
||||
);
|
||||
expect(screen.getByRole("button", { name: /add provider discount/i })).toBeDisabled();
|
||||
});
|
||||
|
||||
it("should disable the submit button when a discount is entered but no provider is selected", () => {
|
||||
renderWithProviders(
|
||||
<AddProviderForm {...DEFAULT_PROPS} selectedProvider={undefined} newDiscount="5" />
|
||||
);
|
||||
expect(screen.getByRole("button", { name: /add provider discount/i })).toBeDisabled();
|
||||
});
|
||||
|
||||
it("should enable the submit button when both a provider and a discount value are provided", () => {
|
||||
renderWithProviders(
|
||||
<AddProviderForm {...DEFAULT_PROPS} selectedProvider="OpenAI" newDiscount="5" />
|
||||
);
|
||||
expect(screen.getByRole("button", { name: /add provider discount/i })).not.toBeDisabled();
|
||||
});
|
||||
|
||||
it("should call onAddProvider when the enabled submit button is clicked", async () => {
|
||||
const onAddProvider = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(
|
||||
<AddProviderForm
|
||||
{...DEFAULT_PROPS}
|
||||
selectedProvider="OpenAI"
|
||||
newDiscount="5"
|
||||
onAddProvider={onAddProvider}
|
||||
/>
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /add provider discount/i }));
|
||||
expect(onAddProvider).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should show the percent sign next to the discount input", () => {
|
||||
renderWithProviders(<AddProviderForm {...DEFAULT_PROPS} />);
|
||||
expect(screen.getByText("%")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
+201
@@ -0,0 +1,201 @@
|
||||
import React from "react";
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { renderWithProviders } from "../../../tests/test-utils";
|
||||
import CostTrackingSettings from "./cost_tracking_settings";
|
||||
|
||||
// Mock sub-hooks so we can control their state without network calls
|
||||
const mockDiscountConfig = vi.fn(() => ({}));
|
||||
const mockMarginConfig = vi.fn(() => ({}));
|
||||
|
||||
vi.mock("./use_discount_config", () => ({
|
||||
useDiscountConfig: () => ({
|
||||
discountConfig: mockDiscountConfig(),
|
||||
fetchDiscountConfig: vi.fn().mockResolvedValue(undefined),
|
||||
handleAddProvider: vi.fn().mockResolvedValue(true),
|
||||
handleRemoveProvider: vi.fn().mockResolvedValue(undefined),
|
||||
handleDiscountChange: vi.fn().mockResolvedValue(undefined),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("./use_margin_config", () => ({
|
||||
useMarginConfig: () => ({
|
||||
marginConfig: mockMarginConfig(),
|
||||
fetchMarginConfig: vi.fn().mockResolvedValue(undefined),
|
||||
handleAddMargin: vi.fn().mockResolvedValue(true),
|
||||
handleRemoveMargin: vi.fn().mockResolvedValue(undefined),
|
||||
handleMarginChange: vi.fn().mockResolvedValue(undefined),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("./pricing_calculator/index", () => ({
|
||||
default: () => <div data-testid="pricing-calculator">Pricing Calculator</div>,
|
||||
}));
|
||||
|
||||
vi.mock("../playground/llm_calls/fetch_models", () => ({
|
||||
fetchAvailableModels: vi.fn().mockResolvedValue([]),
|
||||
}));
|
||||
|
||||
vi.mock("../HelpLink", () => ({
|
||||
DocsMenu: () => null,
|
||||
}));
|
||||
|
||||
vi.mock("./how_it_works", () => ({
|
||||
default: () => <div data-testid="how-it-works">How It Works</div>,
|
||||
}));
|
||||
|
||||
vi.mock("../provider_info_helpers", () => ({
|
||||
Providers: { OpenAI: "OpenAI" },
|
||||
provider_map: { OpenAI: "openai" },
|
||||
providerLogoMap: {},
|
||||
}));
|
||||
|
||||
vi.mock("./provider_display_helpers", () => ({
|
||||
getProviderDisplayInfo: vi.fn(() => ({ displayName: "OpenAI", logo: "", enumKey: "OpenAI" })),
|
||||
handleImageError: vi.fn(),
|
||||
}));
|
||||
|
||||
const ADMIN_PROPS = {
|
||||
userID: "user-1",
|
||||
userRole: "proxy_admin",
|
||||
accessToken: "test-token",
|
||||
};
|
||||
|
||||
describe("CostTrackingSettings", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockDiscountConfig.mockReturnValue({});
|
||||
mockMarginConfig.mockReturnValue({});
|
||||
});
|
||||
|
||||
it("should return nothing when accessToken is null", () => {
|
||||
const { container } = renderWithProviders(
|
||||
<CostTrackingSettings userID="user-1" userRole="proxy_admin" accessToken={null} />
|
||||
);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it("should render the page title", () => {
|
||||
renderWithProviders(<CostTrackingSettings {...ADMIN_PROPS} />);
|
||||
expect(screen.getByText("Cost Tracking Settings")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show the Provider Discounts accordion header for proxy_admin", () => {
|
||||
renderWithProviders(<CostTrackingSettings {...ADMIN_PROPS} />);
|
||||
expect(screen.getByText("Provider Discounts")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show the Fee/Price Margin accordion header for proxy_admin", () => {
|
||||
renderWithProviders(<CostTrackingSettings {...ADMIN_PROPS} />);
|
||||
expect(screen.getByText("Fee/Price Margin")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should always show the Pricing Calculator section", () => {
|
||||
renderWithProviders(<CostTrackingSettings {...ADMIN_PROPS} />);
|
||||
// The accordion header text appears in the DOM; getAllByText tolerates duplicates
|
||||
expect(screen.getAllByText("Pricing Calculator").length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("should show the pricing calculator component", async () => {
|
||||
renderWithProviders(<CostTrackingSettings {...ADMIN_PROPS} />);
|
||||
expect(await screen.findByTestId("pricing-calculator")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should not show Provider Discounts section for a non-admin role", () => {
|
||||
renderWithProviders(
|
||||
<CostTrackingSettings userID="user-1" userRole="internal_user" accessToken="test-token" />
|
||||
);
|
||||
expect(screen.queryByText("Provider Discounts")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should not show Fee/Price Margin section for a non-admin role", () => {
|
||||
renderWithProviders(
|
||||
<CostTrackingSettings userID="user-1" userRole="internal_user" accessToken="test-token" />
|
||||
);
|
||||
expect(screen.queryByText("Fee/Price Margin")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show Provider Discounts for the 'Admin' role as well", () => {
|
||||
renderWithProviders(
|
||||
<CostTrackingSettings userID="user-1" userRole="Admin" accessToken="test-token" />
|
||||
);
|
||||
expect(screen.getByText("Provider Discounts")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show the subtitle describing discount/margin configuration", () => {
|
||||
renderWithProviders(<CostTrackingSettings {...ADMIN_PROPS} />);
|
||||
expect(
|
||||
screen.getByText(/configure cost discounts and margins/i)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("Add Provider Discount modal", () => {
|
||||
it("should open the Add Provider Discount modal when the button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<CostTrackingSettings {...ADMIN_PROPS} />);
|
||||
|
||||
// The button lives inside the Provider Discounts accordion — click the header to expand first
|
||||
const accordionHeader = screen.getByText("Provider Discounts").closest("button");
|
||||
if (accordionHeader) {
|
||||
await user.click(accordionHeader);
|
||||
}
|
||||
|
||||
const addButton = await screen.findByRole("button", { name: /add provider discount/i });
|
||||
await user.click(addButton);
|
||||
|
||||
expect(
|
||||
await screen.findByText("Add Provider Discount", { selector: "h2" })
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Add Provider Margin modal", () => {
|
||||
it("should open the Add Provider Margin modal when the button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<CostTrackingSettings {...ADMIN_PROPS} />);
|
||||
|
||||
const accordionHeader = screen.getByText("Fee/Price Margin").closest("button");
|
||||
if (accordionHeader) {
|
||||
await user.click(accordionHeader);
|
||||
}
|
||||
|
||||
const addButton = await screen.findByRole("button", { name: /add provider margin/i });
|
||||
await user.click(addButton);
|
||||
|
||||
expect(
|
||||
await screen.findByText("Add Provider Margin", { selector: "h2" })
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("empty state messages", () => {
|
||||
it("should show the empty state message when no discount config is loaded", async () => {
|
||||
mockDiscountConfig.mockReturnValue({});
|
||||
renderWithProviders(<CostTrackingSettings {...ADMIN_PROPS} />);
|
||||
|
||||
const accordionHeader = screen.getByText("Provider Discounts").closest("button");
|
||||
if (accordionHeader) {
|
||||
await userEvent.setup().click(accordionHeader);
|
||||
}
|
||||
|
||||
expect(
|
||||
await screen.findByText(/no provider discounts configured/i)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show the empty state message when no margin config is loaded", async () => {
|
||||
mockMarginConfig.mockReturnValue({});
|
||||
renderWithProviders(<CostTrackingSettings {...ADMIN_PROPS} />);
|
||||
|
||||
const accordionHeader = screen.getByText("Fee/Price Margin").closest("button");
|
||||
if (accordionHeader) {
|
||||
await userEvent.setup().click(accordionHeader);
|
||||
}
|
||||
|
||||
expect(
|
||||
await screen.findByText(/no provider margins configured/i)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,95 @@
|
||||
import React from "react";
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { renderWithProviders } from "../../../tests/test-utils";
|
||||
import HowItWorks from "./how_it_works";
|
||||
|
||||
vi.mock("@/app/(dashboard)/api-reference/components/CodeBlock", () => ({
|
||||
default: ({ code }: { code: string }) => <pre data-testid="code-block">{code}</pre>,
|
||||
}));
|
||||
|
||||
describe("HowItWorks", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should render", () => {
|
||||
renderWithProviders(<HowItWorks />);
|
||||
expect(screen.getByText("Cost Calculation")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display the cost calculation formula", () => {
|
||||
renderWithProviders(<HowItWorks />);
|
||||
expect(screen.getByText(/final_cost = base_cost/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display the valid range information", () => {
|
||||
renderWithProviders(<HowItWorks />);
|
||||
expect(screen.getByText(/0% and 100%/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render the code block with a curl example", () => {
|
||||
renderWithProviders(<HowItWorks />);
|
||||
expect(screen.getByTestId("code-block")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("code-block").textContent).toContain("curl");
|
||||
});
|
||||
|
||||
it("should show the response header names for discount verification", () => {
|
||||
renderWithProviders(<HowItWorks />);
|
||||
expect(screen.getByText("x-litellm-response-cost")).toBeInTheDocument();
|
||||
expect(screen.getByText("x-litellm-response-cost-original")).toBeInTheDocument();
|
||||
expect(screen.getByText("x-litellm-response-cost-discount-amount")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should not show calculated results initially when no input is provided", () => {
|
||||
renderWithProviders(<HowItWorks />);
|
||||
expect(screen.queryByText("Calculated Results")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should not show calculated results when only response cost is entered", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<HowItWorks />);
|
||||
|
||||
const responseCostInput = screen.getByPlaceholderText("0.0171938125");
|
||||
await user.type(responseCostInput, "0.01");
|
||||
|
||||
expect(screen.queryByText("Calculated Results")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should not show calculated results when only discount amount is entered", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<HowItWorks />);
|
||||
|
||||
const discountAmountInput = screen.getByPlaceholderText("0.0009049375");
|
||||
await user.type(discountAmountInput, "0.001");
|
||||
|
||||
expect(screen.queryByText("Calculated Results")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show calculated results when both fields are filled", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<HowItWorks />);
|
||||
|
||||
const responseCostInput = screen.getByPlaceholderText("0.0171938125");
|
||||
const discountAmountInput = screen.getByPlaceholderText("0.0009049375");
|
||||
|
||||
await user.type(responseCostInput, "0.0171938125");
|
||||
await user.type(discountAmountInput, "0.0009049375");
|
||||
|
||||
expect(await screen.findByText("Calculated Results")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show original cost, final cost, and discount amount in results", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<HowItWorks />);
|
||||
|
||||
await user.type(screen.getByPlaceholderText("0.0171938125"), "0.0171938125");
|
||||
await user.type(screen.getByPlaceholderText("0.0009049375"), "0.0009049375");
|
||||
|
||||
expect(await screen.findByText("Original Cost:")).toBeInTheDocument();
|
||||
expect(screen.getByText("Final Cost:")).toBeInTheDocument();
|
||||
expect(screen.getByText("Discount Amount:")).toBeInTheDocument();
|
||||
expect(screen.getByText("Discount Applied:")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
+241
@@ -0,0 +1,241 @@
|
||||
import React from "react";
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { screen, within } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { renderWithProviders } from "../../../tests/test-utils";
|
||||
import ProviderDiscountTable from "./provider_discount_table";
|
||||
|
||||
vi.mock("@heroicons/react/outline", () => ({
|
||||
TrashIcon: function TrashIcon() { return null; },
|
||||
PencilAltIcon: function PencilAltIcon() { return null; },
|
||||
CheckIcon: function CheckIcon() { return null; },
|
||||
XIcon: function XIcon() { return null; },
|
||||
}));
|
||||
|
||||
vi.mock("@tremor/react", () => ({
|
||||
Table: ({ children }: any) => <table>{children}</table>,
|
||||
TableHead: ({ children }: any) => <thead>{children}</thead>,
|
||||
TableRow: ({ children }: any) => <tr>{children}</tr>,
|
||||
TableHeaderCell: ({ children }: any) => <th>{children}</th>,
|
||||
TableBody: ({ children }: any) => <tbody>{children}</tbody>,
|
||||
TableCell: ({ children }: any) => <td>{children}</td>,
|
||||
Text: ({ children }: any) => <span>{children}</span>,
|
||||
TextInput: ({ value, onValueChange, onKeyDown, placeholder, ...rest }: any) => (
|
||||
<input
|
||||
value={value}
|
||||
onChange={(e) => onValueChange?.(e.target.value)}
|
||||
onKeyDown={onKeyDown}
|
||||
placeholder={placeholder}
|
||||
{...rest}
|
||||
/>
|
||||
),
|
||||
Icon: ({ icon: IconComponent, onClick }: any) => {
|
||||
const name = IconComponent?.displayName ?? IconComponent?.name ?? "icon";
|
||||
return <button onClick={onClick} aria-label={name} />;
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("./provider_display_helpers", () => ({
|
||||
getProviderDisplayInfo: vi.fn((providerValue: string) => ({
|
||||
displayName: providerValue === "openai" ? "OpenAI" : providerValue,
|
||||
logo: providerValue === "openai" ? "https://example.com/openai.png" : "",
|
||||
enumKey: providerValue === "openai" ? "OpenAI" : null,
|
||||
})),
|
||||
handleImageError: vi.fn(),
|
||||
}));
|
||||
|
||||
const DEFAULT_DISCOUNT_CONFIG = {
|
||||
openai: 0.05,
|
||||
anthropic: 0.1,
|
||||
};
|
||||
|
||||
describe("ProviderDiscountTable", () => {
|
||||
const onDiscountChange = vi.fn();
|
||||
const onRemoveProvider = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should render", () => {
|
||||
renderWithProviders(
|
||||
<ProviderDiscountTable
|
||||
discountConfig={DEFAULT_DISCOUNT_CONFIG}
|
||||
onDiscountChange={onDiscountChange}
|
||||
onRemoveProvider={onRemoveProvider}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByRole("table")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render the table headers", () => {
|
||||
renderWithProviders(
|
||||
<ProviderDiscountTable
|
||||
discountConfig={DEFAULT_DISCOUNT_CONFIG}
|
||||
onDiscountChange={onDiscountChange}
|
||||
onRemoveProvider={onRemoveProvider}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText("Provider")).toBeInTheDocument();
|
||||
expect(screen.getByText("Discount Percentage")).toBeInTheDocument();
|
||||
expect(screen.getByText("Actions")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display provider display names in the table", () => {
|
||||
renderWithProviders(
|
||||
<ProviderDiscountTable
|
||||
discountConfig={DEFAULT_DISCOUNT_CONFIG}
|
||||
onDiscountChange={onDiscountChange}
|
||||
onRemoveProvider={onRemoveProvider}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText("OpenAI")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display the formatted discount percentage", () => {
|
||||
renderWithProviders(
|
||||
<ProviderDiscountTable
|
||||
discountConfig={{ openai: 0.05 }}
|
||||
onDiscountChange={onDiscountChange}
|
||||
onRemoveProvider={onRemoveProvider}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText("5.0%")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show a text input when the edit icon is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(
|
||||
<ProviderDiscountTable
|
||||
discountConfig={{ openai: 0.05 }}
|
||||
onDiscountChange={onDiscountChange}
|
||||
onRemoveProvider={onRemoveProvider}
|
||||
/>
|
||||
);
|
||||
|
||||
const pencilButton = screen.getByRole("button", { name: /PencilAltIcon/i });
|
||||
await user.click(pencilButton);
|
||||
|
||||
expect(screen.getByPlaceholderText("5")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should hide the formatted percentage when in edit mode", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(
|
||||
<ProviderDiscountTable
|
||||
discountConfig={{ openai: 0.05 }}
|
||||
onDiscountChange={onDiscountChange}
|
||||
onRemoveProvider={onRemoveProvider}
|
||||
/>
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /PencilAltIcon/i }));
|
||||
|
||||
expect(screen.queryByText("5.0%")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should call onDiscountChange with the new value when the save icon is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(
|
||||
<ProviderDiscountTable
|
||||
discountConfig={{ openai: 0.05 }}
|
||||
onDiscountChange={onDiscountChange}
|
||||
onRemoveProvider={onRemoveProvider}
|
||||
/>
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /PencilAltIcon/i }));
|
||||
|
||||
const input = screen.getByPlaceholderText("5");
|
||||
await user.clear(input);
|
||||
await user.type(input, "10");
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /CheckIcon/i }));
|
||||
|
||||
expect(onDiscountChange).toHaveBeenCalledWith("openai", "0.1");
|
||||
});
|
||||
|
||||
it("should restore the display view after saving", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(
|
||||
<ProviderDiscountTable
|
||||
discountConfig={{ openai: 0.05 }}
|
||||
onDiscountChange={onDiscountChange}
|
||||
onRemoveProvider={onRemoveProvider}
|
||||
/>
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /PencilAltIcon/i }));
|
||||
await user.click(screen.getByRole("button", { name: /CheckIcon/i }));
|
||||
|
||||
expect(screen.queryByPlaceholderText("5")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should cancel edit mode when the cancel icon is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(
|
||||
<ProviderDiscountTable
|
||||
discountConfig={{ openai: 0.05 }}
|
||||
onDiscountChange={onDiscountChange}
|
||||
onRemoveProvider={onRemoveProvider}
|
||||
/>
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /PencilAltIcon/i }));
|
||||
await user.click(screen.getByRole("button", { name: /XIcon/i }));
|
||||
|
||||
expect(screen.queryByPlaceholderText("5")).not.toBeInTheDocument();
|
||||
expect(onDiscountChange).not.toHaveBeenCalled();
|
||||
expect(screen.getByText("5.0%")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should not call onDiscountChange when canceling edit", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(
|
||||
<ProviderDiscountTable
|
||||
discountConfig={{ openai: 0.05 }}
|
||||
onDiscountChange={onDiscountChange}
|
||||
onRemoveProvider={onRemoveProvider}
|
||||
/>
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /PencilAltIcon/i }));
|
||||
await user.click(screen.getByRole("button", { name: /XIcon/i }));
|
||||
|
||||
expect(onDiscountChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should call onRemoveProvider with the provider key and display name when the trash icon is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(
|
||||
<ProviderDiscountTable
|
||||
discountConfig={{ openai: 0.05 }}
|
||||
onDiscountChange={onDiscountChange}
|
||||
onRemoveProvider={onRemoveProvider}
|
||||
/>
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /TrashIcon/i }));
|
||||
|
||||
expect(onRemoveProvider).toHaveBeenCalledWith("openai", "OpenAI");
|
||||
});
|
||||
|
||||
it("should not call onDiscountChange when the entered value is out of range", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(
|
||||
<ProviderDiscountTable
|
||||
discountConfig={{ openai: 0.05 }}
|
||||
onDiscountChange={onDiscountChange}
|
||||
onRemoveProvider={onRemoveProvider}
|
||||
/>
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /PencilAltIcon/i }));
|
||||
const input = screen.getByPlaceholderText("5");
|
||||
await user.clear(input);
|
||||
await user.type(input, "150");
|
||||
await user.click(screen.getByRole("button", { name: /CheckIcon/i }));
|
||||
|
||||
expect(onDiscountChange).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { getProviderDisplayInfo, getProviderBackendValue, handleImageError } from "./provider_display_helpers";
|
||||
|
||||
vi.mock("../provider_info_helpers", () => ({
|
||||
Providers: {
|
||||
OpenAI: "OpenAI",
|
||||
Anthropic: "Anthropic",
|
||||
Azure: "Azure",
|
||||
},
|
||||
provider_map: {
|
||||
OpenAI: "openai",
|
||||
Anthropic: "anthropic",
|
||||
Azure: "azure",
|
||||
},
|
||||
providerLogoMap: {
|
||||
OpenAI: "https://example.com/openai.png",
|
||||
Anthropic: "https://example.com/anthropic.png",
|
||||
Azure: "https://example.com/azure.png",
|
||||
},
|
||||
}));
|
||||
|
||||
describe("getProviderDisplayInfo", () => {
|
||||
it("should return display name and logo for a known backend provider value", () => {
|
||||
const info = getProviderDisplayInfo("openai");
|
||||
expect(info.displayName).toBe("OpenAI");
|
||||
expect(info.logo).toBe("https://example.com/openai.png");
|
||||
expect(info.enumKey).toBe("OpenAI");
|
||||
});
|
||||
|
||||
it("should return the raw value as display name for an unknown provider", () => {
|
||||
const info = getProviderDisplayInfo("my-custom-provider");
|
||||
expect(info.displayName).toBe("my-custom-provider");
|
||||
expect(info.logo).toBe("");
|
||||
expect(info.enumKey).toBeNull();
|
||||
});
|
||||
|
||||
it("should match a provider by its backend value regardless of casing", () => {
|
||||
const info = getProviderDisplayInfo("anthropic");
|
||||
expect(info.displayName).toBe("Anthropic");
|
||||
expect(info.enumKey).toBe("Anthropic");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getProviderBackendValue", () => {
|
||||
it("should return the backend value for a known provider enum key", () => {
|
||||
expect(getProviderBackendValue("OpenAI")).toBe("openai");
|
||||
});
|
||||
|
||||
it("should return the backend value for another known provider", () => {
|
||||
expect(getProviderBackendValue("Anthropic")).toBe("anthropic");
|
||||
});
|
||||
|
||||
it("should return null for an unknown enum key", () => {
|
||||
expect(getProviderBackendValue("UnknownProvider")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("handleImageError", () => {
|
||||
it("should replace the img element with a fallback div showing the first letter", () => {
|
||||
const img = document.createElement("img");
|
||||
const parent = document.createElement("div");
|
||||
parent.appendChild(img);
|
||||
|
||||
const event = { target: img } as any;
|
||||
handleImageError(event, "OpenAI");
|
||||
|
||||
expect(parent.querySelector("img")).toBeNull();
|
||||
const fallback = parent.firstChild as HTMLElement;
|
||||
expect(fallback.tagName).toBe("DIV");
|
||||
expect(fallback.textContent).toBe("O");
|
||||
});
|
||||
|
||||
it("should use the first character of the fallback text as the label", () => {
|
||||
const img = document.createElement("img");
|
||||
const parent = document.createElement("div");
|
||||
parent.appendChild(img);
|
||||
|
||||
const event = { target: img } as any;
|
||||
handleImageError(event, "Anthropic");
|
||||
|
||||
const fallback = parent.firstChild as HTMLElement;
|
||||
expect(fallback.textContent).toBe("A");
|
||||
});
|
||||
|
||||
it("should do nothing if the image has no parent element", () => {
|
||||
const img = document.createElement("img");
|
||||
const event = { target: img } as any;
|
||||
// Should not throw
|
||||
expect(() => handleImageError(event, "OpenAI")).not.toThrow();
|
||||
});
|
||||
});
|
||||
+246
@@ -0,0 +1,246 @@
|
||||
import React from "react";
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { renderWithProviders } from "../../../tests/test-utils";
|
||||
import ProviderMarginTable from "./provider_margin_table";
|
||||
|
||||
vi.mock("@heroicons/react/outline", () => ({
|
||||
TrashIcon: function TrashIcon() { return null; },
|
||||
PencilAltIcon: function PencilAltIcon() { return null; },
|
||||
CheckIcon: function CheckIcon() { return null; },
|
||||
XIcon: function XIcon() { return null; },
|
||||
}));
|
||||
|
||||
vi.mock("@tremor/react", () => ({
|
||||
Table: ({ children }: any) => <table>{children}</table>,
|
||||
TableHead: ({ children }: any) => <thead>{children}</thead>,
|
||||
TableRow: ({ children }: any) => <tr>{children}</tr>,
|
||||
TableHeaderCell: ({ children }: any) => <th>{children}</th>,
|
||||
TableBody: ({ children }: any) => <tbody>{children}</tbody>,
|
||||
TableCell: ({ children }: any) => <td>{children}</td>,
|
||||
Text: ({ children }: any) => <span>{children}</span>,
|
||||
TextInput: ({ value, onValueChange, placeholder, autoFocus, className }: any) => (
|
||||
<input
|
||||
value={value}
|
||||
onChange={(e) => onValueChange?.(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
autoFocus={autoFocus}
|
||||
className={className}
|
||||
/>
|
||||
),
|
||||
Icon: ({ icon: IconComponent, onClick }: any) => {
|
||||
const name = IconComponent?.displayName ?? IconComponent?.name ?? "icon";
|
||||
return <button onClick={onClick} aria-label={name} />;
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("./provider_display_helpers", () => ({
|
||||
getProviderDisplayInfo: vi.fn((providerValue: string) => {
|
||||
if (providerValue === "openai") return { displayName: "OpenAI", logo: "", enumKey: "OpenAI" };
|
||||
if (providerValue === "anthropic") return { displayName: "Anthropic", logo: "", enumKey: "Anthropic" };
|
||||
return { displayName: providerValue, logo: "", enumKey: null };
|
||||
}),
|
||||
handleImageError: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("ProviderMarginTable", () => {
|
||||
const onMarginChange = vi.fn();
|
||||
const onRemoveProvider = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should render", () => {
|
||||
renderWithProviders(
|
||||
<ProviderMarginTable
|
||||
marginConfig={{ openai: 0.1 }}
|
||||
onMarginChange={onMarginChange}
|
||||
onRemoveProvider={onRemoveProvider}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByRole("table")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render the table headers", () => {
|
||||
renderWithProviders(
|
||||
<ProviderMarginTable
|
||||
marginConfig={{ openai: 0.1 }}
|
||||
onMarginChange={onMarginChange}
|
||||
onRemoveProvider={onRemoveProvider}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText("Provider")).toBeInTheDocument();
|
||||
expect(screen.getByText("Margin")).toBeInTheDocument();
|
||||
expect(screen.getByText("Actions")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display the provider display name", () => {
|
||||
renderWithProviders(
|
||||
<ProviderMarginTable
|
||||
marginConfig={{ openai: 0.1 }}
|
||||
onMarginChange={onMarginChange}
|
||||
onRemoveProvider={onRemoveProvider}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText("OpenAI")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display the global provider as 'Global (All Providers)'", () => {
|
||||
renderWithProviders(
|
||||
<ProviderMarginTable
|
||||
marginConfig={{ global: 0.05 }}
|
||||
onMarginChange={onMarginChange}
|
||||
onRemoveProvider={onRemoveProvider}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText("Global (All Providers)")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display a numeric margin as a percentage", () => {
|
||||
renderWithProviders(
|
||||
<ProviderMarginTable
|
||||
marginConfig={{ openai: 0.1 }}
|
||||
onMarginChange={onMarginChange}
|
||||
onRemoveProvider={onRemoveProvider}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText("10.0%")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display a fixed amount margin with dollar sign", () => {
|
||||
renderWithProviders(
|
||||
<ProviderMarginTable
|
||||
marginConfig={{ openai: { fixed_amount: 0.001 } }}
|
||||
onMarginChange={onMarginChange}
|
||||
onRemoveProvider={onRemoveProvider}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText("$0.001000")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display a combined percentage and fixed margin", () => {
|
||||
renderWithProviders(
|
||||
<ProviderMarginTable
|
||||
marginConfig={{ openai: { percentage: 0.1, fixed_amount: 0.001 } }}
|
||||
onMarginChange={onMarginChange}
|
||||
onRemoveProvider={onRemoveProvider}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText(/10\.0%.*\$0\.001000/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show edit inputs when the pencil icon is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(
|
||||
<ProviderMarginTable
|
||||
marginConfig={{ openai: 0.1 }}
|
||||
onMarginChange={onMarginChange}
|
||||
onRemoveProvider={onRemoveProvider}
|
||||
/>
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /PencilAltIcon/i }));
|
||||
|
||||
expect(screen.getByPlaceholderText("10")).toBeInTheDocument();
|
||||
expect(screen.getByPlaceholderText("0.001")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should call onMarginChange with a percentage value when save is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(
|
||||
<ProviderMarginTable
|
||||
marginConfig={{ openai: 0.1 }}
|
||||
onMarginChange={onMarginChange}
|
||||
onRemoveProvider={onRemoveProvider}
|
||||
/>
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /PencilAltIcon/i }));
|
||||
|
||||
const percentInput = screen.getByPlaceholderText("10");
|
||||
await user.clear(percentInput);
|
||||
await user.type(percentInput, "20");
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /CheckIcon/i }));
|
||||
|
||||
expect(onMarginChange).toHaveBeenCalledWith("openai", 0.2);
|
||||
});
|
||||
|
||||
it("should cancel edit mode without calling onMarginChange when X is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(
|
||||
<ProviderMarginTable
|
||||
marginConfig={{ openai: 0.1 }}
|
||||
onMarginChange={onMarginChange}
|
||||
onRemoveProvider={onRemoveProvider}
|
||||
/>
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /PencilAltIcon/i }));
|
||||
await user.click(screen.getByRole("button", { name: /XIcon/i }));
|
||||
|
||||
expect(onMarginChange).not.toHaveBeenCalled();
|
||||
expect(screen.queryByPlaceholderText("10")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should call onRemoveProvider with provider key and display name when trash is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(
|
||||
<ProviderMarginTable
|
||||
marginConfig={{ openai: 0.1 }}
|
||||
onMarginChange={onMarginChange}
|
||||
onRemoveProvider={onRemoveProvider}
|
||||
/>
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /TrashIcon/i }));
|
||||
|
||||
expect(onRemoveProvider).toHaveBeenCalledWith("openai", "OpenAI");
|
||||
});
|
||||
|
||||
it("should call onRemoveProvider with 'Global' display name for the global provider", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(
|
||||
<ProviderMarginTable
|
||||
marginConfig={{ global: 0.05 }}
|
||||
onMarginChange={onMarginChange}
|
||||
onRemoveProvider={onRemoveProvider}
|
||||
/>
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /TrashIcon/i }));
|
||||
|
||||
expect(onRemoveProvider).toHaveBeenCalledWith("global", "Global");
|
||||
});
|
||||
|
||||
describe("when both percentage and fixed amount are entered", () => {
|
||||
it("should call onMarginChange with an object containing both values", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(
|
||||
<ProviderMarginTable
|
||||
marginConfig={{ openai: 0.1 }}
|
||||
onMarginChange={onMarginChange}
|
||||
onRemoveProvider={onRemoveProvider}
|
||||
/>
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /PencilAltIcon/i }));
|
||||
|
||||
const percentInput = screen.getByPlaceholderText("10");
|
||||
await user.clear(percentInput);
|
||||
await user.type(percentInput, "5");
|
||||
|
||||
const fixedInput = screen.getByPlaceholderText("0.001");
|
||||
await user.type(fixedInput, "0.002");
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /CheckIcon/i }));
|
||||
|
||||
expect(onMarginChange).toHaveBeenCalledWith("openai", {
|
||||
percentage: 0.05,
|
||||
fixed_amount: 0.002,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,244 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { renderHook, act } from "@testing-library/react";
|
||||
import { useDiscountConfig } from "./use_discount_config";
|
||||
import NotificationsManager from "@/components/molecules/notifications_manager";
|
||||
|
||||
vi.mock("@/components/networking", () => ({
|
||||
getProxyBaseUrl: vi.fn(() => ""),
|
||||
getGlobalLitellmHeaderName: vi.fn(() => "Authorization"),
|
||||
}));
|
||||
|
||||
vi.mock("./provider_display_helpers", () => ({
|
||||
getProviderBackendValue: vi.fn((enumKey: string) => {
|
||||
const map: Record<string, string> = {
|
||||
OpenAI: "openai",
|
||||
Anthropic: "anthropic",
|
||||
};
|
||||
return map[enumKey] ?? null;
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("../provider_info_helpers", () => ({
|
||||
Providers: {
|
||||
OpenAI: "OpenAI",
|
||||
Anthropic: "Anthropic",
|
||||
},
|
||||
}));
|
||||
|
||||
describe("useDiscountConfig", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("fetchDiscountConfig", () => {
|
||||
it("should populate discountConfig with fetched values on success", async () => {
|
||||
vi.spyOn(global, "fetch").mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ values: { openai: 0.05, anthropic: 0.1 } }),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useDiscountConfig({ accessToken: "test-token" }));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.fetchDiscountConfig();
|
||||
});
|
||||
|
||||
expect(result.current.discountConfig).toEqual({ openai: 0.05, anthropic: 0.1 });
|
||||
});
|
||||
|
||||
it("should set an empty config when the response has no values", async () => {
|
||||
vi.spyOn(global, "fetch").mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ values: {} }),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useDiscountConfig({ accessToken: "test-token" }));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.fetchDiscountConfig();
|
||||
});
|
||||
|
||||
expect(result.current.discountConfig).toEqual({});
|
||||
});
|
||||
|
||||
it("should show an error notification when the fetch throws", async () => {
|
||||
vi.spyOn(global, "fetch").mockRejectedValueOnce(new Error("Network error"));
|
||||
|
||||
const { result } = renderHook(() => useDiscountConfig({ accessToken: "test-token" }));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.fetchDiscountConfig();
|
||||
});
|
||||
|
||||
expect(NotificationsManager.fromBackend).toHaveBeenCalledWith(
|
||||
expect.stringMatching(/failed to fetch/i)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("handleAddProvider", () => {
|
||||
it("should return false and notify when no provider is selected", async () => {
|
||||
const { result } = renderHook(() => useDiscountConfig({ accessToken: "test-token" }));
|
||||
|
||||
let success: boolean;
|
||||
await act(async () => {
|
||||
success = await result.current.handleAddProvider(undefined, "5");
|
||||
});
|
||||
|
||||
expect(success!).toBe(false);
|
||||
expect(NotificationsManager.fromBackend).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should return false and notify when no discount is provided", async () => {
|
||||
const { result } = renderHook(() => useDiscountConfig({ accessToken: "test-token" }));
|
||||
|
||||
let success: boolean;
|
||||
await act(async () => {
|
||||
success = await result.current.handleAddProvider("OpenAI", "");
|
||||
});
|
||||
|
||||
expect(success!).toBe(false);
|
||||
expect(NotificationsManager.fromBackend).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should return false and notify when the discount exceeds 100", async () => {
|
||||
const { result } = renderHook(() => useDiscountConfig({ accessToken: "test-token" }));
|
||||
|
||||
let success: boolean;
|
||||
await act(async () => {
|
||||
success = await result.current.handleAddProvider("OpenAI", "150");
|
||||
});
|
||||
|
||||
expect(success!).toBe(false);
|
||||
expect(NotificationsManager.fromBackend).toHaveBeenCalledWith(
|
||||
expect.stringMatching(/0%.*100%/i)
|
||||
);
|
||||
});
|
||||
|
||||
it("should return false and notify when the provider already exists in the config", async () => {
|
||||
vi.spyOn(global, "fetch")
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ values: { openai: 0.05 } }),
|
||||
} as Response)
|
||||
.mockResolvedValue({ ok: true, json: async () => ({}) } as Response);
|
||||
|
||||
const { result } = renderHook(() => useDiscountConfig({ accessToken: "test-token" }));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.fetchDiscountConfig();
|
||||
});
|
||||
|
||||
let success: boolean;
|
||||
await act(async () => {
|
||||
success = await result.current.handleAddProvider("OpenAI", "10");
|
||||
});
|
||||
|
||||
expect(success!).toBe(false);
|
||||
expect(NotificationsManager.fromBackend).toHaveBeenCalledWith(
|
||||
expect.stringMatching(/already exists/i)
|
||||
);
|
||||
});
|
||||
|
||||
it("should save the config and return true on a valid new provider", async () => {
|
||||
vi.spyOn(global, "fetch")
|
||||
.mockResolvedValueOnce({ ok: true, json: async () => ({ values: {} }) } as Response)
|
||||
.mockResolvedValueOnce({ ok: true, json: async () => ({}) } as Response)
|
||||
.mockResolvedValueOnce({ ok: true, json: async () => ({ values: { openai: 0.05 } }) } as Response);
|
||||
|
||||
const { result } = renderHook(() => useDiscountConfig({ accessToken: "test-token" }));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.fetchDiscountConfig();
|
||||
});
|
||||
|
||||
let success: boolean;
|
||||
await act(async () => {
|
||||
success = await result.current.handleAddProvider("OpenAI", "5");
|
||||
});
|
||||
|
||||
expect(success!).toBe(true);
|
||||
expect(NotificationsManager.success).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("handleRemoveProvider", () => {
|
||||
it("should remove the provider from the config and save", async () => {
|
||||
vi.spyOn(global, "fetch")
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ values: { openai: 0.05, anthropic: 0.1 } }),
|
||||
} as Response)
|
||||
.mockResolvedValueOnce({ ok: true, json: async () => ({}) } as Response)
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ values: { anthropic: 0.1 } }),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useDiscountConfig({ accessToken: "test-token" }));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.fetchDiscountConfig();
|
||||
});
|
||||
|
||||
expect(result.current.discountConfig).toHaveProperty("openai");
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleRemoveProvider("openai");
|
||||
});
|
||||
|
||||
// The optimistic update removes openai immediately
|
||||
expect(result.current.discountConfig).not.toHaveProperty("openai");
|
||||
});
|
||||
});
|
||||
|
||||
describe("handleDiscountChange", () => {
|
||||
it("should update the discount value and save when the value is valid", async () => {
|
||||
vi.spyOn(global, "fetch")
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ values: { openai: 0.05 } }),
|
||||
} as Response)
|
||||
.mockResolvedValueOnce({ ok: true, json: async () => ({}) } as Response)
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ values: { openai: 0.1 } }),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useDiscountConfig({ accessToken: "test-token" }));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.fetchDiscountConfig();
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleDiscountChange("openai", "0.1");
|
||||
});
|
||||
|
||||
// Optimistic update applied immediately
|
||||
expect(result.current.discountConfig["openai"]).toBe(0.1);
|
||||
});
|
||||
|
||||
it("should not save when the value is greater than 1 (invalid fraction)", async () => {
|
||||
vi.spyOn(global, "fetch").mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ values: { openai: 0.05 } }),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useDiscountConfig({ accessToken: "test-token" }));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.fetchDiscountConfig();
|
||||
});
|
||||
|
||||
// Clear mocks after the initial fetch so we can check that no PATCH was made
|
||||
vi.clearAllMocks();
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleDiscountChange("openai", "1.5");
|
||||
});
|
||||
|
||||
expect(global.fetch).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,316 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { renderHook, act } from "@testing-library/react";
|
||||
import { useMarginConfig } from "./use_margin_config";
|
||||
import NotificationsManager from "@/components/molecules/notifications_manager";
|
||||
|
||||
vi.mock("@/components/networking", () => ({
|
||||
getProxyBaseUrl: vi.fn(() => ""),
|
||||
getGlobalLitellmHeaderName: vi.fn(() => "Authorization"),
|
||||
}));
|
||||
|
||||
vi.mock("./provider_display_helpers", () => ({
|
||||
getProviderBackendValue: vi.fn((enumKey: string) => {
|
||||
const map: Record<string, string> = {
|
||||
OpenAI: "openai",
|
||||
Anthropic: "anthropic",
|
||||
};
|
||||
return map[enumKey] ?? null;
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("../provider_info_helpers", () => ({
|
||||
Providers: {
|
||||
OpenAI: "OpenAI",
|
||||
Anthropic: "Anthropic",
|
||||
},
|
||||
}));
|
||||
|
||||
describe("useMarginConfig", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("fetchMarginConfig", () => {
|
||||
it("should populate marginConfig with fetched values on success", async () => {
|
||||
vi.spyOn(global, "fetch").mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ values: { openai: 0.1, global: 0.05 } }),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useMarginConfig({ accessToken: "test-token" }));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.fetchMarginConfig();
|
||||
});
|
||||
|
||||
expect(result.current.marginConfig).toEqual({ openai: 0.1, global: 0.05 });
|
||||
});
|
||||
|
||||
it("should set an empty config when the response has no values", async () => {
|
||||
vi.spyOn(global, "fetch").mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ values: {} }),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useMarginConfig({ accessToken: "test-token" }));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.fetchMarginConfig();
|
||||
});
|
||||
|
||||
expect(result.current.marginConfig).toEqual({});
|
||||
});
|
||||
|
||||
it("should show an error notification when fetch throws", async () => {
|
||||
vi.spyOn(global, "fetch").mockRejectedValueOnce(new Error("Network error"));
|
||||
|
||||
const { result } = renderHook(() => useMarginConfig({ accessToken: "test-token" }));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.fetchMarginConfig();
|
||||
});
|
||||
|
||||
expect(NotificationsManager.fromBackend).toHaveBeenCalledWith(
|
||||
expect.stringMatching(/failed to fetch/i)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("handleAddMargin", () => {
|
||||
it("should return false and notify when no provider is selected", async () => {
|
||||
const { result } = renderHook(() => useMarginConfig({ accessToken: "test-token" }));
|
||||
|
||||
let success: boolean;
|
||||
await act(async () => {
|
||||
success = await result.current.handleAddMargin({
|
||||
selectedProvider: undefined,
|
||||
marginType: "percentage",
|
||||
percentageValue: "10",
|
||||
fixedAmountValue: "",
|
||||
});
|
||||
});
|
||||
|
||||
expect(success!).toBe(false);
|
||||
expect(NotificationsManager.fromBackend).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should return false and notify when percentage is out of range", async () => {
|
||||
const { result } = renderHook(() => useMarginConfig({ accessToken: "test-token" }));
|
||||
|
||||
let success: boolean;
|
||||
await act(async () => {
|
||||
success = await result.current.handleAddMargin({
|
||||
selectedProvider: "OpenAI",
|
||||
marginType: "percentage",
|
||||
percentageValue: "2000",
|
||||
fixedAmountValue: "",
|
||||
});
|
||||
});
|
||||
|
||||
expect(success!).toBe(false);
|
||||
expect(NotificationsManager.fromBackend).toHaveBeenCalledWith(
|
||||
expect.stringMatching(/0%.*1000%/i)
|
||||
);
|
||||
});
|
||||
|
||||
it("should return false when the provider already has a margin configured", async () => {
|
||||
vi.spyOn(global, "fetch")
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ values: { openai: 0.05 } }),
|
||||
} as Response)
|
||||
.mockResolvedValue({ ok: true, json: async () => ({}) } as Response);
|
||||
|
||||
const { result } = renderHook(() => useMarginConfig({ accessToken: "test-token" }));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.fetchMarginConfig();
|
||||
});
|
||||
|
||||
let success: boolean;
|
||||
await act(async () => {
|
||||
success = await result.current.handleAddMargin({
|
||||
selectedProvider: "OpenAI",
|
||||
marginType: "percentage",
|
||||
percentageValue: "10",
|
||||
fixedAmountValue: "",
|
||||
});
|
||||
});
|
||||
|
||||
expect(success!).toBe(false);
|
||||
expect(NotificationsManager.fromBackend).toHaveBeenCalledWith(
|
||||
expect.stringMatching(/already exists/i)
|
||||
);
|
||||
});
|
||||
|
||||
it("should save a percentage margin and return true for a valid new provider", async () => {
|
||||
vi.spyOn(global, "fetch")
|
||||
.mockResolvedValueOnce({ ok: true, json: async () => ({ values: {} }) } as Response)
|
||||
.mockResolvedValueOnce({ ok: true, json: async () => ({}) } as Response)
|
||||
.mockResolvedValueOnce({ ok: true, json: async () => ({ values: { openai: 0.1 } }) } as Response);
|
||||
|
||||
const { result } = renderHook(() => useMarginConfig({ accessToken: "test-token" }));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.fetchMarginConfig();
|
||||
});
|
||||
|
||||
let success: boolean;
|
||||
await act(async () => {
|
||||
success = await result.current.handleAddMargin({
|
||||
selectedProvider: "OpenAI",
|
||||
marginType: "percentage",
|
||||
percentageValue: "10",
|
||||
fixedAmountValue: "",
|
||||
});
|
||||
});
|
||||
|
||||
expect(success!).toBe(true);
|
||||
expect(NotificationsManager.success).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should save a fixed amount margin and return true for a valid new provider", async () => {
|
||||
vi.spyOn(global, "fetch")
|
||||
.mockResolvedValueOnce({ ok: true, json: async () => ({ values: {} }) } as Response)
|
||||
.mockResolvedValueOnce({ ok: true, json: async () => ({}) } as Response)
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ values: { openai: { fixed_amount: 0.001 } } }),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useMarginConfig({ accessToken: "test-token" }));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.fetchMarginConfig();
|
||||
});
|
||||
|
||||
let success: boolean;
|
||||
await act(async () => {
|
||||
success = await result.current.handleAddMargin({
|
||||
selectedProvider: "OpenAI",
|
||||
marginType: "fixed",
|
||||
percentageValue: "",
|
||||
fixedAmountValue: "0.001",
|
||||
});
|
||||
});
|
||||
|
||||
expect(success!).toBe(true);
|
||||
expect(NotificationsManager.success).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should accept the global provider without provider_map lookup", async () => {
|
||||
vi.spyOn(global, "fetch")
|
||||
.mockResolvedValueOnce({ ok: true, json: async () => ({ values: {} }) } as Response)
|
||||
.mockResolvedValueOnce({ ok: true, json: async () => ({}) } as Response)
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ values: { global: 0.05 } }),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useMarginConfig({ accessToken: "test-token" }));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.fetchMarginConfig();
|
||||
});
|
||||
|
||||
let success: boolean;
|
||||
await act(async () => {
|
||||
success = await result.current.handleAddMargin({
|
||||
selectedProvider: "global",
|
||||
marginType: "percentage",
|
||||
percentageValue: "5",
|
||||
fixedAmountValue: "",
|
||||
});
|
||||
});
|
||||
|
||||
expect(success!).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("handleRemoveMargin", () => {
|
||||
it("should remove the provider from the config and save", async () => {
|
||||
vi.spyOn(global, "fetch")
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ values: { openai: 0.1, anthropic: 0.05 } }),
|
||||
} as Response)
|
||||
.mockResolvedValueOnce({ ok: true, json: async () => ({}) } as Response)
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ values: { anthropic: 0.05 } }),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useMarginConfig({ accessToken: "test-token" }));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.fetchMarginConfig();
|
||||
});
|
||||
|
||||
expect(result.current.marginConfig).toHaveProperty("openai");
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleRemoveMargin("openai");
|
||||
});
|
||||
|
||||
expect(result.current.marginConfig).not.toHaveProperty("openai");
|
||||
});
|
||||
});
|
||||
|
||||
describe("handleMarginChange", () => {
|
||||
it("should update the margin value for a provider and save", async () => {
|
||||
vi.spyOn(global, "fetch")
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ values: { openai: 0.1 } }),
|
||||
} as Response)
|
||||
.mockResolvedValueOnce({ ok: true, json: async () => ({}) } as Response)
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ values: { openai: 0.2 } }),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useMarginConfig({ accessToken: "test-token" }));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.fetchMarginConfig();
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleMarginChange("openai", 0.2);
|
||||
});
|
||||
|
||||
// Optimistic update applied immediately
|
||||
expect(result.current.marginConfig["openai"]).toBe(0.2);
|
||||
});
|
||||
|
||||
it("should update the margin with a complex value (percentage + fixed)", async () => {
|
||||
vi.spyOn(global, "fetch")
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ values: { openai: 0.1 } }),
|
||||
} as Response)
|
||||
.mockResolvedValueOnce({ ok: true, json: async () => ({}) } as Response)
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
values: { openai: { percentage: 0.05, fixed_amount: 0.001 } },
|
||||
}),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useMarginConfig({ accessToken: "test-token" }));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.fetchMarginConfig();
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleMarginChange("openai", { percentage: 0.05, fixed_amount: 0.001 });
|
||||
});
|
||||
|
||||
expect(result.current.marginConfig["openai"]).toEqual({
|
||||
percentage: 0.05,
|
||||
fixed_amount: 0.001,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user