diff --git a/ui/litellm-dashboard/src/app/(dashboard)/organizations/OrganizationFilters.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/organizations/OrganizationFilters.test.tsx index 37eeaf4c2af..c1eda1be670 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/organizations/OrganizationFilters.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/organizations/OrganizationFilters.test.tsx @@ -106,7 +106,7 @@ describe("OrganizationFilters", () => { org_alias: "test org", }; - render( + const { container } = render( { />, ); - const filtersButton = screen.getByRole("button", { name: /^filters$/i }); - const badgeWrapper = filtersButton.closest(".ant-badge"); - expect(badgeWrapper).toBeInTheDocument(); + expect(screen.getByRole("button", { name: /^filters$/i })).toBeInTheDocument(); + expect(container.querySelector("sup")).toBeInTheDocument(); }); }); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/vector-stores/_components/TestVectorStoreTab.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/vector-stores/_components/TestVectorStoreTab.test.tsx index c1322dced60..1d7bce34e04 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/vector-stores/_components/TestVectorStoreTab.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/vector-stores/_components/TestVectorStoreTab.test.tsx @@ -1,4 +1,5 @@ -import { render, screen, fireEvent } from "@testing-library/react"; +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; import { describe, it, expect, vi } from "vitest"; import TestVectorStoreTab from "./TestVectorStoreTab"; import { VectorStore } from "@/components/vector_store_management/types"; @@ -60,31 +61,24 @@ describe("TestVectorStoreTab", () => { expect(screen.getByTestId("tester-access-token")).toHaveTextContent("test-token"); }); - it("should update VectorStoreTester when selecting different vector store", () => { + it("should update VectorStoreTester when selecting different vector store", async () => { + const user = userEvent.setup(); render(); - // Find the select component - const selectElement = screen.getByRole("combobox"); + await user.click(screen.getByRole("combobox")); + await user.click(await screen.findByText("Test Store 2")); - // Change selection - fireEvent.mouseDown(selectElement); - - // Wait for options to appear and click the second one - const option2 = screen.getByText("Test Store 2"); - fireEvent.click(option2); - - // Verify the tester component updated expect(screen.getByTestId("tester-vector-store-id")).toHaveTextContent("vs_456"); }); - it("should display vector store names in select options", () => { + it("should display vector store names in select options", async () => { + const user = userEvent.setup(); render(); - const selectElement = screen.getByRole("combobox"); - fireEvent.mouseDown(selectElement); + await user.click(screen.getByRole("combobox")); - // Use getAllByText since the selected value also shows the name - expect(screen.getAllByText("Test Store 1").length).toBeGreaterThan(0); + // The selected store's name may also render in the trigger, so only require at least one match. + expect((await screen.findAllByText("Test Store 1")).length).toBeGreaterThan(0); expect(screen.getByText("Test Store 2")).toBeInTheDocument(); }); }); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/vector-stores/_components/VectorStoreTester.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/vector-stores/_components/VectorStoreTester.test.tsx new file mode 100644 index 00000000000..cbabcc6dca5 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/vector-stores/_components/VectorStoreTester.test.tsx @@ -0,0 +1,156 @@ +import { render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { vectorStoreSearchCall } from "@/components/networking"; + +import { VectorStoreTester } from "./VectorStoreTester"; + +vi.mock("@/components/networking", () => ({ + vectorStoreSearchCall: vi.fn(), +})); + +const mockWarning = vi.fn(); +vi.mock("@/components/molecules/message_manager", () => ({ + __esModule: true, + default: { warning: (...args: unknown[]) => mockWarning(...args) }, +})); + +const mockFromBackend = vi.fn(); +const mockSuccess = vi.fn(); +vi.mock("@/components/molecules/notifications_manager", () => ({ + __esModule: true, + default: { + fromBackend: (...args: unknown[]) => mockFromBackend(...args), + success: (...args: unknown[]) => mockSuccess(...args), + }, +})); + +const mockSearch = vi.mocked(vectorStoreSearchCall); + +const searchResponse = { + object: "vector_store.search_results.page", + search_query: "hello", + data: [ + { + score: 0.91234, + content: [{ text: "the quick brown fox", type: "text" }], + file_id: "file-1", + filename: "notes.txt", + attributes: { source: "manual" }, + }, + ], +}; + +const EMPTY_STATE = "Test your vector store by entering a search query below"; + +const renderTester = () => render(); + +const queryInput = () => screen.getByPlaceholderText(/enter your search query/i); +const searchButton = () => screen.getByRole("button", { name: /search/i }); + +describe("VectorStoreTester", () => { + beforeEach(() => { + vi.clearAllMocks(); + mockSearch.mockResolvedValue(searchResponse); + }); + + it("shows the empty state before any search has run", () => { + renderTester(); + expect(screen.getByText(EMPTY_STATE)).toBeInTheDocument(); + expect(screen.queryByRole("button", { name: /clear history/i })).not.toBeInTheDocument(); + }); + + it("does not search until a non-blank query is entered", async () => { + const user = userEvent.setup(); + renderTester(); + + await user.click(searchButton()); + expect(mockSearch).not.toHaveBeenCalled(); + + await user.type(queryInput(), "hello"); + await user.click(searchButton()); + + await waitFor(() => expect(mockSearch).toHaveBeenCalledWith("sk-test", "vs_123", "hello")); + }); + + it("renders the returned result and clears the query input", async () => { + const user = userEvent.setup(); + renderTester(); + + await user.type(queryInput(), "hello"); + await user.click(searchButton()); + + expect(await screen.findByText("Result 1")).toBeInTheDocument(); + expect(screen.getByText("1 results")).toBeInTheDocument(); + expect(screen.getByText("Score: 0.9123")).toBeInTheDocument(); + expect(screen.queryByText(EMPTY_STATE)).not.toBeInTheDocument(); + await waitFor(() => expect(queryInput()).toHaveValue("")); + }); + + it("expands a result to reveal its content and metadata", async () => { + const user = userEvent.setup(); + renderTester(); + + await user.type(queryInput(), "hello"); + await user.click(searchButton()); + + expect(await screen.findByText("Result 1")).toBeInTheDocument(); + expect(screen.queryByText("the quick brown fox")).not.toBeInTheDocument(); + + await user.click(screen.getByText("Result 1")); + + expect(screen.getByText("the quick brown fox")).toBeInTheDocument(); + expect(screen.getByText("File ID:").parentElement).toHaveTextContent("file-1"); + expect(screen.getByText("Filename:").parentElement).toHaveTextContent("notes.txt"); + }); + + it("warns instead of searching when the query is only whitespace", async () => { + const user = userEvent.setup(); + renderTester(); + + await user.type(queryInput(), " "); + await user.type(queryInput(), "{Enter}"); + + expect(mockWarning).toHaveBeenCalledWith("Please enter a search query"); + expect(mockSearch).not.toHaveBeenCalled(); + }); + + it("submits on Enter but not on Shift+Enter", async () => { + const user = userEvent.setup(); + renderTester(); + + await user.type(queryInput(), "hello"); + await user.type(queryInput(), "{Shift>}{Enter}{/Shift}"); + expect(mockSearch).not.toHaveBeenCalled(); + + await user.type(queryInput(), "{Enter}"); + await waitFor(() => expect(mockSearch).toHaveBeenCalledTimes(1)); + }); + + it("reports a failed search and keeps the history empty", async () => { + const user = userEvent.setup(); + mockSearch.mockRejectedValue(new Error("boom")); + renderTester(); + + await user.type(queryInput(), "hello"); + await user.click(searchButton()); + + await waitFor(() => expect(mockFromBackend).toHaveBeenCalledWith("Failed to search vector store")); + expect(screen.getByText(EMPTY_STATE)).toBeInTheDocument(); + }); + + it("clears the search history", async () => { + const user = userEvent.setup(); + renderTester(); + + await user.type(queryInput(), "hello"); + await user.click(searchButton()); + expect(await screen.findByText("Result 1")).toBeInTheDocument(); + + await user.click(screen.getByRole("button", { name: /clear history/i })); + + expect(screen.queryByText("Result 1")).not.toBeInTheDocument(); + expect(screen.getByText(EMPTY_STATE)).toBeInTheDocument(); + }); +}); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/vector-stores/_components/index.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/vector-stores/_components/index.test.tsx index 2931372f384..521c1f879ee 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/vector-stores/_components/index.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/vector-stores/_components/index.test.tsx @@ -1,4 +1,5 @@ import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { vectorStoreListCall } from "@/components/networking"; @@ -25,18 +26,25 @@ vi.mock("./TestVectorStoreTab", () => ({ __esModule: true, default: () => null } const mockVectorStoreListCall = vi.mocked(vectorStoreListCall); +const openManageTab = async (user: ReturnType) => { + await user.click(screen.getByRole("tab", { name: "Manage Vector Stores" })); +}; + describe("VectorStoreManagement loading state", () => { beforeEach(() => { vi.clearAllMocks(); }); it("should resolve the loading state when accessToken is null instead of showing the skeleton forever", async () => { + const user = userEvent.setup(); render(); + await openManageTab(user); expect(await screen.findByText("table-loaded")).toBeInTheDocument(); expect(mockVectorStoreListCall).not.toHaveBeenCalled(); }); it("should show the loading state until the vector store fetch settles", async () => { + const user = userEvent.setup(); let resolveFetch: (value: { data: never[] }) => void = () => {}; mockVectorStoreListCall.mockReturnValue( new Promise((resolve) => { @@ -44,6 +52,7 @@ describe("VectorStoreManagement loading state", () => { }), ); render(); + await openManageTab(user); expect(screen.getByText("table-loading")).toBeInTheDocument(); resolveFetch({ data: [] }); diff --git a/ui/litellm-dashboard/src/components/common_components/Filters/FiltersButton.test.tsx b/ui/litellm-dashboard/src/components/common_components/Filters/FiltersButton.test.tsx index ccb2c5d9e53..145fdb1f576 100644 --- a/ui/litellm-dashboard/src/components/common_components/Filters/FiltersButton.test.tsx +++ b/ui/litellm-dashboard/src/components/common_components/Filters/FiltersButton.test.tsx @@ -21,12 +21,18 @@ describe("FiltersButton", () => { expect(onClick).toHaveBeenCalledTimes(1); }); - it("should show badge when hasActiveFilters is true", () => { + it("should show the active-filter indicator when hasActiveFilters is true", () => { const onClick = vi.fn(); const { container } = render(); - const button = screen.getByRole("button", { name: /filters/i }); - const badgeWrapper = button.closest(".ant-badge"); - expect(badgeWrapper).toBeInTheDocument(); + expect(screen.getByRole("button", { name: /filters/i })).toBeInTheDocument(); + expect(container.querySelector("sup")).toBeInTheDocument(); + }); + + it("should not show the active-filter indicator when hasActiveFilters is false", () => { + const onClick = vi.fn(); + const { container } = render(); + expect(screen.getByRole("button", { name: /filters/i })).toBeInTheDocument(); + expect(container.querySelector("sup")).not.toBeInTheDocument(); }); it("should render custom label when provided", () => {