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 new file mode 100644 index 0000000000..b244736540 --- /dev/null +++ b/ui/litellm-dashboard/src/components/view_logs/SpendLogsSettingsModal/SpendLogsSettingsModal.test.tsx @@ -0,0 +1,348 @@ +import { render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { beforeEach, describe, expect, it, vi, Mock } from "vitest"; +import SpendLogsSettingsModal from "./SpendLogsSettingsModal"; +import { useStoreRequestInSpendLogs } from "@/app/(dashboard)/hooks/storeRequestInSpendLogs/useStoreRequestInSpendLogs"; +import NotificationsManager from "@/components/molecules/notifications_manager"; +import { parseErrorMessage } from "@/components/shared/errorUtils"; +import { renderWithProviders } from "../../../../tests/test-utils"; + +vi.mock("@/app/(dashboard)/hooks/storeRequestInSpendLogs/useStoreRequestInSpendLogs"); +vi.mock("@/components/molecules/notifications_manager", () => ({ + default: { + success: vi.fn(), + fromBackend: vi.fn(), + }, +})); +vi.mock("@/components/shared/errorUtils", () => ({ + parseErrorMessage: vi.fn(), +})); + +const mockUseStoreRequestInSpendLogs = vi.mocked(useStoreRequestInSpendLogs); +const mockNotificationsManager = vi.mocked(NotificationsManager); +const mockParseErrorMessage = vi.mocked(parseErrorMessage); + +describe("SpendLogsSettingsModal", () => { + const mockOnCancel = vi.fn(); + const mockOnSuccess = vi.fn(); + const mockMutateAsync = vi.fn(); + + const defaultProps = { + isVisible: true, + onCancel: mockOnCancel, + onSuccess: mockOnSuccess, + }; + + beforeEach(() => { + vi.clearAllMocks(); + mockUseStoreRequestInSpendLogs.mockReturnValue({ + mutateAsync: mockMutateAsync, + isPending: false, + } as any); + mockParseErrorMessage.mockImplementation((error: any) => error?.message || String(error)); + }); + + it("should render the modal", () => { + renderWithProviders(); + expect(screen.getByRole("dialog")).toBeInTheDocument(); + expect(screen.getByText("Spend Logs Settings")).toBeInTheDocument(); + }); + + it("should render form fields with initial values", () => { + renderWithProviders(); + + expect(screen.getByText("Store Prompts in Spend Logs")).toBeInTheDocument(); + expect(screen.getByLabelText("Maximum Spend Logs Retention Period (Optional)")).toBeInTheDocument(); + expect(screen.getByPlaceholderText("e.g., 7d, 30d")).toBeInTheDocument(); + }); + + it("should render cancel and save buttons", () => { + renderWithProviders(); + + expect(screen.getByRole("button", { name: "Cancel" })).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Save Settings" })).toBeInTheDocument(); + }); + + it("should call onCancel when cancel button is clicked", async () => { + const user = userEvent.setup(); + renderWithProviders(); + + const cancelButton = screen.getByRole("button", { name: "Cancel" }); + await user.click(cancelButton); + + expect(mockOnCancel).toHaveBeenCalledTimes(1); + }); + + it("should call onCancel when modal close button is clicked", async () => { + const user = userEvent.setup(); + renderWithProviders(); + + const closeButton = screen.getByRole("button", { name: /close/i }); + await user.click(closeButton); + + expect(mockOnCancel).toHaveBeenCalledTimes(1); + }); + + it("should toggle store prompts switch", async () => { + const user = userEvent.setup(); + renderWithProviders(); + + const switchElement = screen.getByRole("switch"); + expect(switchElement).not.toBeChecked(); + + await user.click(switchElement); + + await waitFor(() => { + expect(switchElement).toBeChecked(); + }); + }); + + it("should update retention period input", async () => { + const user = userEvent.setup(); + renderWithProviders(); + + const retentionInput = screen.getByPlaceholderText("e.g., 7d, 30d"); + await user.type(retentionInput, "30d"); + + expect(retentionInput).toHaveValue("30d"); + }); + + it("should submit form with store prompts enabled and retention period", async () => { + const user = userEvent.setup(); + mockMutateAsync.mockImplementation(async (params, options) => { + await Promise.resolve(); + options?.onSuccess?.(); + return { message: "Success" }; + }); + + renderWithProviders(); + + const switchElement = screen.getByRole("switch"); + await user.click(switchElement); + + const retentionInput = screen.getByPlaceholderText("e.g., 7d, 30d"); + await user.type(retentionInput, "30d"); + + const saveButton = screen.getByRole("button", { name: "Save Settings" }); + await user.click(saveButton); + + await waitFor(() => { + expect(mockMutateAsync).toHaveBeenCalledWith( + { + store_prompts_in_spend_logs: true, + maximum_spend_logs_retention_period: "30d", + }, + expect.any(Object) + ); + }); + }); + + it("should submit form with store prompts disabled and no retention period", async () => { + const user = userEvent.setup(); + 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(mockMutateAsync).toHaveBeenCalledWith( + { + store_prompts_in_spend_logs: false, + maximum_spend_logs_retention_period: undefined, + }, + expect.any(Object) + ); + }); + }); + + it("should show success notification and call onSuccess on successful submission", async () => { + const user = userEvent.setup(); + 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(mockNotificationsManager.success).toHaveBeenCalledWith("Spend logs settings updated successfully"); + expect(mockOnSuccess).toHaveBeenCalledTimes(1); + }); + }); + + it("should show error notification when submission fails", async () => { + const user = userEvent.setup(); + const error = new Error("Network error"); + mockMutateAsync.mockRejectedValue(error); + mockParseErrorMessage.mockReturnValue("Network error"); + + renderWithProviders(); + + const saveButton = screen.getByRole("button", { name: "Save Settings" }); + await user.click(saveButton); + + await waitFor(() => { + expect(mockNotificationsManager.fromBackend).toHaveBeenCalledWith("Failed to save spend logs settings: Network error"); + }); + }); + + it("should show error notification from onError callback", async () => { + const user = userEvent.setup(); + const error = new Error("Backend error"); + mockMutateAsync.mockImplementation((params, options) => { + options?.onError?.(error); + return Promise.reject(error); + }); + mockParseErrorMessage.mockReturnValue("Backend error"); + + renderWithProviders(); + + const saveButton = screen.getByRole("button", { name: "Save Settings" }); + await user.click(saveButton); + + await waitFor(() => { + expect(mockNotificationsManager.fromBackend).toHaveBeenCalledWith("Failed to save spend logs settings: Backend error"); + }); + }); + + it("should disable cancel button when pending", () => { + mockUseStoreRequestInSpendLogs.mockReturnValue({ + mutateAsync: mockMutateAsync, + isPending: true, + } 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, + 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(); + + const switchElement = screen.getByRole("switch"); + await user.click(switchElement); + + const retentionInput = screen.getByPlaceholderText("e.g., 7d, 30d"); + await user.type(retentionInput, "30d"); + + expect(switchElement).toBeChecked(); + expect(retentionInput).toHaveValue("30d"); + + const cancelButton = screen.getByRole("button", { name: "Cancel" }); + await user.click(cancelButton); + + expect(mockOnCancel).toHaveBeenCalledTimes(1); + }); + + it("should reset form fields after successful submission", async () => { + const user = userEvent.setup(); + mockMutateAsync.mockImplementation(async (params, options) => { + await Promise.resolve(); + options?.onSuccess?.(); + return { message: "Success" }; + }); + + const { rerender } = renderWithProviders(); + + const switchElement = screen.getByRole("switch"); + await user.click(switchElement); + + const retentionInput = screen.getByPlaceholderText("e.g., 7d, 30d"); + await user.type(retentionInput, "30d"); + + expect(switchElement).toBeChecked(); + expect(retentionInput).toHaveValue("30d"); + + const saveButton = screen.getByRole("button", { name: "Save Settings" }); + await user.click(saveButton); + + 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(""); + }); + }); + + it("should not call onSuccess when it is not provided", async () => { + const user = userEvent.setup(); + 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(mockNotificationsManager.success).toHaveBeenCalled(); + }); + }); + + it("should not render modal when isVisible is false", () => { + renderWithProviders(); + + expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); + }); + + it("should submit form with only store prompts enabled and no retention period", async () => { + const user = userEvent.setup(); + mockMutateAsync.mockImplementation(async (params, options) => { + await Promise.resolve(); + options?.onSuccess?.(); + return { message: "Success" }; + }); + + renderWithProviders(); + + const switchElement = screen.getByRole("switch"); + await user.click(switchElement); + + const saveButton = screen.getByRole("button", { name: "Save Settings" }); + await user.click(saveButton); + + await waitFor(() => { + expect(mockMutateAsync).toHaveBeenCalledWith( + { + store_prompts_in_spend_logs: true, + maximum_spend_logs_retention_period: undefined, + }, + expect.any(Object) + ); + }); + }); +});