From f231d46375e9e56553ef50ca96dccba4af94beac Mon Sep 17 00:00:00 2001 From: Yuneng Jiang Date: Thu, 23 Jul 2026 22:26:34 -0700 Subject: [PATCH] test(ui): decouple access-groups, vector-stores and organizations tests from antd markup Prepares the shadcn migration of these three routes by removing every assertion that depends on the current component library, so the same tests can gate the migration without being edited. FiltersButton and its OrganizationFilters consumer both asserted on the ".ant-badge" wrapper class; they now assert the active-filter indicator element itself, and FiltersButton additionally asserts that it is absent when there are no active filters. TestVectorStoreTab drove the antd Select with fireEvent.mouseDown and picked options by node; it now clicks through the combobox role and the option text, which works against any listbox implementation. The vector-stores index test relied on Tremor mounting every TabPanel at once, so it read the Manage tab's table without ever opening that tab. It now clicks the tab first, which is what a user does and what any tabs implementation supports. VectorStoreTester had no test at all, so this adds a characterisation suite covering the empty state, the blank-query guard, the search call and its rendered result, result expansion, Enter versus Shift+Enter, the failure path and clearing history. All of these pass against the current antd and Tremor components --- .../OrganizationFilters.test.tsx | 7 +- .../_components/TestVectorStoreTab.test.tsx | 28 ++-- .../_components/VectorStoreTester.test.tsx | 156 ++++++++++++++++++ .../vector-stores/_components/index.test.tsx | 9 + .../Filters/FiltersButton.test.tsx | 14 +- 5 files changed, 189 insertions(+), 25 deletions(-) create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/vector-stores/_components/VectorStoreTester.test.tsx 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", () => {