diff --git a/ui/litellm-dashboard/src/components/view_logs/SpendLogsSettingsModal/SpendLogsSettingsModal.test.tsx b/ui/litellm-dashboard/src/components/view_logs/SpendLogsSettingsModal/SpendLogsSettingsModal.test.tsx
index e955f42872..40d06d9046 100644
--- a/ui/litellm-dashboard/src/components/view_logs/SpendLogsSettingsModal/SpendLogsSettingsModal.test.tsx
+++ b/ui/litellm-dashboard/src/components/view_logs/SpendLogsSettingsModal/SpendLogsSettingsModal.test.tsx
@@ -1,3 +1,4 @@
+import { useDeleteProxyConfigField, useProxyConfig } from "@/app/(dashboard)/hooks/proxyConfig/useProxyConfig";
import { useStoreRequestInSpendLogs } from "@/app/(dashboard)/hooks/storeRequestInSpendLogs/useStoreRequestInSpendLogs";
import NotificationsManager from "@/components/molecules/notifications_manager";
import { parseErrorMessage } from "@/components/shared/errorUtils";
@@ -8,6 +9,7 @@ import { renderWithProviders } from "../../../../tests/test-utils";
import SpendLogsSettingsModal from "./SpendLogsSettingsModal";
vi.mock("@/app/(dashboard)/hooks/storeRequestInSpendLogs/useStoreRequestInSpendLogs");
+vi.mock("@/app/(dashboard)/hooks/proxyConfig/useProxyConfig");
vi.mock("@/components/molecules/notifications_manager", () => ({
default: {
success: vi.fn(),
@@ -19,6 +21,8 @@ vi.mock("@/components/shared/errorUtils", () => ({
}));
const mockUseStoreRequestInSpendLogs = vi.mocked(useStoreRequestInSpendLogs);
+const mockUseProxyConfig = vi.mocked(useProxyConfig);
+const mockUseDeleteProxyConfigField = vi.mocked(useDeleteProxyConfigField);
const mockNotificationsManager = vi.mocked(NotificationsManager);
const mockParseErrorMessage = vi.mocked(parseErrorMessage);
@@ -26,6 +30,8 @@ describe("SpendLogsSettingsModal", () => {
const mockOnCancel = vi.fn();
const mockOnSuccess = vi.fn();
const mockMutateAsync = vi.fn();
+ const mockDeleteField = vi.fn();
+ const mockRefetch = vi.fn();
const defaultProps = {
isVisible: true,
@@ -39,6 +45,15 @@ describe("SpendLogsSettingsModal", () => {
mutateAsync: mockMutateAsync,
isPending: false,
} as any);
+ mockUseDeleteProxyConfigField.mockReturnValue({
+ mutateAsync: mockDeleteField,
+ isPending: false,
+ } as any);
+ mockUseProxyConfig.mockReturnValue({
+ data: [],
+ isLoading: false,
+ refetch: mockRefetch,
+ } as any);
mockParseErrorMessage.mockImplementation((error: any) => error?.message || String(error));
});
@@ -127,6 +142,7 @@ describe("SpendLogsSettingsModal", () => {
await user.click(saveButton);
await waitFor(() => {
+ expect(mockDeleteField).not.toHaveBeenCalled();
expect(mockMutateAsync).toHaveBeenCalledWith(
{
store_prompts_in_spend_logs: true,
@@ -139,6 +155,7 @@ describe("SpendLogsSettingsModal", () => {
it("should submit form with store prompts disabled and no retention period", async () => {
const user = userEvent.setup();
+ mockDeleteField.mockResolvedValue({ message: "Field deleted successfully" });
mockMutateAsync.mockImplementation(async (params, options) => {
await Promise.resolve();
options?.onSuccess?.();
@@ -151,10 +168,10 @@ describe("SpendLogsSettingsModal", () => {
await user.click(saveButton);
await waitFor(() => {
+ expect(mockDeleteField).toHaveBeenCalled();
expect(mockMutateAsync).toHaveBeenCalledWith(
{
store_prompts_in_spend_logs: false,
- maximum_spend_logs_retention_period: undefined,
},
expect.any(Object)
);
@@ -163,6 +180,7 @@ describe("SpendLogsSettingsModal", () => {
it("should show success notification and call onSuccess on successful submission", async () => {
const user = userEvent.setup();
+ mockDeleteField.mockResolvedValue({ message: "Field deleted successfully" });
mockMutateAsync.mockImplementation(async (params, options) => {
await Promise.resolve();
options?.onSuccess?.();
@@ -176,6 +194,7 @@ describe("SpendLogsSettingsModal", () => {
await waitFor(() => {
expect(mockNotificationsManager.success).toHaveBeenCalledWith("Spend logs settings updated successfully");
+ expect(mockRefetch).toHaveBeenCalled();
expect(mockOnSuccess).toHaveBeenCalledTimes(1);
});
});
@@ -227,6 +246,31 @@ describe("SpendLogsSettingsModal", () => {
expect(cancelButton).toBeDisabled();
});
+ it("should disable cancel button when deleting field", () => {
+ mockUseDeleteProxyConfigField.mockReturnValue({
+ mutateAsync: mockDeleteField,
+ isPending: true,
+ } as any);
+
+ renderWithProviders();
+
+ const cancelButton = screen.getByRole("button", { name: "Cancel" });
+ expect(cancelButton).toBeDisabled();
+ });
+
+ it("should disable cancel button when loading config", () => {
+ mockUseProxyConfig.mockReturnValue({
+ data: undefined,
+ isLoading: true,
+ refetch: mockRefetch,
+ } as any);
+
+ renderWithProviders();
+
+ const cancelButton = screen.getByRole("button", { name: "Cancel" });
+ expect(cancelButton).toBeDisabled();
+ });
+
it("should show loading state on save button when pending", () => {
mockUseStoreRequestInSpendLogs.mockReturnValue({
mutateAsync: mockMutateAsync,
@@ -240,6 +284,19 @@ describe("SpendLogsSettingsModal", () => {
expect(saveButton.className).toContain("ant-btn-loading");
});
+ it("should show loading state on save button when deleting field", () => {
+ mockUseDeleteProxyConfigField.mockReturnValue({
+ mutateAsync: mockDeleteField,
+ isPending: true,
+ } as any);
+
+ renderWithProviders();
+
+ const saveButton = screen.getByRole("button", { name: /Saving/i });
+ expect(saveButton).toBeInTheDocument();
+ expect(saveButton.className).toContain("ant-btn-loading");
+ });
+
it("should call onCancel when cancel button is clicked after modifying form", async () => {
const user = userEvent.setup();
renderWithProviders();
@@ -259,15 +316,16 @@ describe("SpendLogsSettingsModal", () => {
expect(mockOnCancel).toHaveBeenCalledTimes(1);
});
- it("should reset form fields after successful submission", async () => {
+ it("should call refetch after successful submission", async () => {
const user = userEvent.setup();
+ mockDeleteField.mockResolvedValue({ message: "Field deleted successfully" });
mockMutateAsync.mockImplementation(async (params, options) => {
await Promise.resolve();
options?.onSuccess?.();
return { message: "Success" };
});
- const { rerender } = renderWithProviders();
+ renderWithProviders();
const switchElement = screen.getByRole("switch");
await user.click(switchElement);
@@ -283,20 +341,13 @@ describe("SpendLogsSettingsModal", () => {
await waitFor(() => {
expect(mockNotificationsManager.success).toHaveBeenCalled();
- });
-
- rerender();
-
- await waitFor(() => {
- const updatedSwitchElement = screen.getByRole("switch");
- const updatedRetentionInput = screen.getByPlaceholderText("e.g., 7d, 30d");
- expect(updatedSwitchElement).not.toBeChecked();
- expect(updatedRetentionInput).toHaveValue("");
+ expect(mockRefetch).toHaveBeenCalled();
});
});
it("should not call onSuccess when it is not provided", async () => {
const user = userEvent.setup();
+ mockDeleteField.mockResolvedValue({ message: "Field deleted successfully" });
mockMutateAsync.mockImplementation(async (params, options) => {
await Promise.resolve();
options?.onSuccess?.();
@@ -319,8 +370,93 @@ describe("SpendLogsSettingsModal", () => {
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
});
+ it("should call refetch when modal opens", () => {
+ renderWithProviders();
+
+ expect(mockRefetch).toHaveBeenCalledTimes(1);
+ });
+
+ it("should render form with initial values from config data", () => {
+ mockUseProxyConfig.mockReturnValue({
+ data: [
+ {
+ field_name: "store_prompts_in_spend_logs",
+ field_type: "bool",
+ field_description: "Store prompts in spend logs",
+ field_value: true,
+ stored_in_db: true,
+ field_default_value: false,
+ },
+ {
+ field_name: "maximum_spend_logs_retention_period",
+ field_type: "string",
+ field_description: "Maximum retention period",
+ field_value: "30d",
+ stored_in_db: true,
+ field_default_value: undefined,
+ },
+ ],
+ isLoading: false,
+ refetch: mockRefetch,
+ } as any);
+
+ renderWithProviders();
+
+ const switchElement = screen.getByRole("switch");
+ const retentionInput = screen.getByPlaceholderText("e.g., 7d, 30d");
+
+ expect(switchElement).toBeChecked();
+ expect(retentionInput).toHaveValue("30d");
+ });
+
+ it("should show skeleton loaders when config is loading", () => {
+ mockUseProxyConfig.mockReturnValue({
+ data: undefined,
+ isLoading: true,
+ refetch: mockRefetch,
+ } as any);
+
+ renderWithProviders();
+
+ // Check that switch and input are not present when loading (skeletons are shown instead)
+ expect(screen.queryByRole("switch")).not.toBeInTheDocument();
+ expect(screen.queryByPlaceholderText("e.g., 7d, 30d")).not.toBeInTheDocument();
+
+ // Check for skeleton elements (Ant Design Skeleton.Input renders with ant-skeleton class)
+ const skeletons = document.querySelectorAll(".ant-skeleton");
+ expect(skeletons.length).toBeGreaterThan(0);
+ });
+
+ it("should continue with update even if deleteField fails", async () => {
+ const user = userEvent.setup();
+ const deleteError = new Error("Field does not exist");
+ mockDeleteField.mockRejectedValue(deleteError);
+ mockMutateAsync.mockImplementation(async (params, options) => {
+ await Promise.resolve();
+ options?.onSuccess?.();
+ return { message: "Success" };
+ });
+
+ renderWithProviders();
+
+ const saveButton = screen.getByRole("button", { name: "Save Settings" });
+ await user.click(saveButton);
+
+ await waitFor(() => {
+ expect(mockDeleteField).toHaveBeenCalled();
+ expect(mockMutateAsync).toHaveBeenCalledWith(
+ {
+ store_prompts_in_spend_logs: false,
+ },
+ expect.any(Object)
+ );
+ expect(mockNotificationsManager.success).toHaveBeenCalled();
+ });
+ });
+
it("should submit form with only store prompts enabled and no retention period", async () => {
const user = userEvent.setup();
+ mockDeleteField.mockResolvedValue({ message: "Field deleted successfully" });
mockMutateAsync.mockImplementation(async (params, options) => {
await Promise.resolve();
options?.onSuccess?.();
@@ -336,10 +472,10 @@ describe("SpendLogsSettingsModal", () => {
await user.click(saveButton);
await waitFor(() => {
+ expect(mockDeleteField).toHaveBeenCalled();
expect(mockMutateAsync).toHaveBeenCalledWith(
{
store_prompts_in_spend_logs: true,
- maximum_spend_logs_retention_period: undefined,
},
expect.any(Object)
);