diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/ModelRetrySettingsTab.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/ModelRetrySettingsTab.test.tsx index d0c27b9fd44..32e619d084c 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/ModelRetrySettingsTab.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/ModelRetrySettingsTab.test.tsx @@ -34,19 +34,19 @@ const buildProps = (overrides: Record = {}) => ({ }); describe("ModelRetrySettingsTab", () => { - it("renders the 'Global Retry Policy' heading when selectedModelGroup is 'global'", () => { + it("should render the 'Global Retry Policy' heading when selectedModelGroup is 'global'", () => { render(); expect(screen.getByText("Global Retry Policy")).toBeInTheDocument(); }); - it("renders a model-specific heading when a model group is selected", () => { + it("should render a model-specific heading when a model group is selected", () => { render(); expect(screen.getByText("Retry Policy for gpt-4")).toBeInTheDocument(); }); - it("renders a row for every error type in the retry policy map", () => { + it("should render a row for every error type in the retry policy map", () => { render(); expect(screen.getByText(/BadRequestError \(400\)/)).toBeInTheDocument(); @@ -57,7 +57,7 @@ describe("ModelRetrySettingsTab", () => { expect(screen.getByText(/InternalServerError \(500\)/)).toBeInTheDocument(); }); - it("uses defaultRetry when globalRetryPolicy is null (global scope)", () => { + it("should use defaultRetry when globalRetryPolicy is null (global scope)", () => { render(); // All 6 spinbutton inputs should show the defaultRetry value @@ -67,7 +67,7 @@ describe("ModelRetrySettingsTab", () => { }); }); - it("shows globalRetryPolicy values when they are set (global scope)", () => { + it("should show globalRetryPolicy values when they are set (global scope)", () => { const globalRetryPolicy: GlobalRetryPolicy = { RateLimitErrorRetries: 5, }; @@ -82,7 +82,7 @@ describe("ModelRetrySettingsTab", () => { expect(inputs[0]).toHaveValue("0"); }); - it("falls back to globalRetryPolicy when no model-specific value is set (model scope)", () => { + it("should fall back to globalRetryPolicy when no model-specific value is set (model scope)", () => { const globalRetryPolicy: GlobalRetryPolicy = { TimeoutErrorRetries: 7, }; @@ -105,7 +105,7 @@ describe("ModelRetrySettingsTab", () => { expect(inputs[0]).toHaveValue("1"); }); - it("prefers model-specific retry count over the global value (model scope)", () => { + it("should prefer model-specific retry count over the global value (model scope)", () => { const globalRetryPolicy: GlobalRetryPolicy = { RateLimitErrorRetries: 3, }; @@ -128,7 +128,7 @@ describe("ModelRetrySettingsTab", () => { expect(inputs[3]).toHaveValue("9"); }); - it("shows the global reference value text for each row in model-specific scope", () => { + it("should show the global reference value text for each row in model-specific scope", () => { const globalRetryPolicy: GlobalRetryPolicy = { BadRequestErrorRetries: 2 }; render( { expect(screen.getByText("(Global: 2)")).toBeInTheDocument(); }); - it("does not show global reference annotations in global scope", () => { + it("should not show global reference annotations in global scope", () => { render(); expect(screen.queryByText(/Global:/)).not.toBeInTheDocument(); }); - it("calls handleSaveRetrySettings when the Save button is clicked", async () => { + it("should call handleSaveRetrySettings when the Save button is clicked", async () => { const user = userEvent.setup(); const handleSaveRetrySettings = vi.fn(); render(); @@ -160,7 +160,7 @@ describe("ModelRetrySettingsTab", () => { expect(handleSaveRetrySettings).toHaveBeenCalledTimes(1); }); - it("calls setGlobalRetryPolicy with an updater function when an input changes (global scope)", async () => { + it("should call setGlobalRetryPolicy with an updater function when an input changes (global scope)", async () => { const user = userEvent.setup(); const setGlobalRetryPolicy = vi.fn(); render( @@ -188,7 +188,7 @@ describe("ModelRetrySettingsTab", () => { expect(result).toMatchObject({ BadRequestErrorRetries: 4 }); }); - it("calls setModelGroupRetryPolicy with an updater function when an input changes (model scope)", async () => { + it("should call setModelGroupRetryPolicy with an updater function when an input changes (model scope)", async () => { const user = userEvent.setup(); const setModelGroupRetryPolicy = vi.fn(); render( diff --git a/ui/litellm-dashboard/src/app/(dashboard)/teams/components/TeamsFilters.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/teams/components/TeamsFilters.test.tsx index ed4e0eac494..9a818c27624 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/teams/components/TeamsFilters.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/teams/components/TeamsFilters.test.tsx @@ -1,4 +1,4 @@ -import { render, screen } from "@testing-library/react"; +import { render, screen, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import React from "react"; import { describe, expect, it, vi } from "vitest"; @@ -39,7 +39,7 @@ const renderFilters = (overrides: Partial[0]> = }; describe("TeamsFilters", () => { - it("renders the team name search input, Filters button, and Reset Filters button", () => { + it("should render the team name search input, Filters button, and Reset Filters button", () => { renderFilters(); expect(screen.getByPlaceholderText("Search by Team Name...")).toBeInTheDocument(); @@ -47,13 +47,13 @@ describe("TeamsFilters", () => { expect(screen.getByRole("button", { name: /reset filters/i })).toBeInTheDocument(); }); - it("reflects the current team_alias filter value in the search input", () => { + it("should reflect the current team_alias filter value in the search input", () => { renderFilters({ filters: { ...emptyFilters, team_alias: "Platform" } }); expect(screen.getByPlaceholderText("Search by Team Name...")).toHaveValue("Platform"); }); - it("calls onChange with 'team_alias' key when the search input changes", async () => { + it("should call onChange with 'team_alias' key when the search input changes", async () => { const user = userEvent.setup(); const onChange = vi.fn(); renderFilters({ onChange }); @@ -63,7 +63,7 @@ describe("TeamsFilters", () => { expect(onChange).toHaveBeenCalledWith("team_alias", expect.stringContaining("D")); }); - it("calls onToggleFilters with the inverted boolean when the Filters button is clicked", async () => { + it("should call onToggleFilters with the inverted boolean when the Filters button is clicked", async () => { const user = userEvent.setup(); const onToggleFilters = vi.fn(); renderFilters({ showFilters: false, onToggleFilters }); @@ -73,7 +73,7 @@ describe("TeamsFilters", () => { expect(onToggleFilters).toHaveBeenCalledWith(true); }); - it("calls onToggleFilters(false) when filters are currently expanded", async () => { + it("should call onToggleFilters(false) when filters are currently expanded", async () => { const user = userEvent.setup(); const onToggleFilters = vi.fn(); renderFilters({ showFilters: true, onToggleFilters }); @@ -83,7 +83,7 @@ describe("TeamsFilters", () => { expect(onToggleFilters).toHaveBeenCalledWith(false); }); - it("calls onReset when the Reset Filters button is clicked", async () => { + it("should call onReset when the Reset Filters button is clicked", async () => { const user = userEvent.setup(); const onReset = vi.fn(); renderFilters({ onReset }); @@ -93,19 +93,19 @@ describe("TeamsFilters", () => { expect(onReset).toHaveBeenCalledTimes(1); }); - it("does not show the Team ID input when showFilters is false", () => { + it("should not show the Team ID input when showFilters is false", () => { renderFilters({ showFilters: false }); expect(screen.queryByPlaceholderText("Enter Team ID")).not.toBeInTheDocument(); }); - it("shows the Team ID input when showFilters is true", () => { + it("should show the Team ID input when showFilters is true", () => { renderFilters({ showFilters: true }); expect(screen.getByPlaceholderText("Enter Team ID")).toBeInTheDocument(); }); - it("calls onChange with 'team_id' key when the Team ID input changes", async () => { + it("should call onChange with 'team_id' key when the Team ID input changes", async () => { const user = userEvent.setup(); const onChange = vi.fn(); renderFilters({ showFilters: true, onChange }); @@ -115,37 +115,37 @@ describe("TeamsFilters", () => { expect(onChange).toHaveBeenCalledWith("team_id", expect.stringContaining("a")); }); - it("reflects the current team_id filter value in the Team ID input", () => { + it("should reflect the current team_id filter value in the Team ID input", () => { renderFilters({ showFilters: true, filters: { ...emptyFilters, team_id: "team-xyz" } }); expect(screen.getByPlaceholderText("Enter Team ID")).toHaveValue("team-xyz"); }); - it("shows a blue dot indicator on the Filters button when team_alias filter is active", () => { + it("should show the active filter indicator on the Filters button when team_alias is set", () => { renderFilters({ filters: { ...emptyFilters, team_alias: "Platform" } }); const filtersButton = screen.getByRole("button", { name: /^filters$/i }); - expect(filtersButton.querySelector(".bg-blue-500")).toBeInTheDocument(); + expect(within(filtersButton).getByTestId("active-filter-indicator")).toBeInTheDocument(); }); - it("shows a blue dot indicator on the Filters button when team_id filter is active", () => { + it("should show the active filter indicator on the Filters button when team_id is set", () => { renderFilters({ filters: { ...emptyFilters, team_id: "team-123" } }); const filtersButton = screen.getByRole("button", { name: /^filters$/i }); - expect(filtersButton.querySelector(".bg-blue-500")).toBeInTheDocument(); + expect(within(filtersButton).getByTestId("active-filter-indicator")).toBeInTheDocument(); }); - it("shows a blue dot indicator on the Filters button when organization_id filter is active", () => { + it("should show the active filter indicator on the Filters button when organization_id is set", () => { renderFilters({ filters: { ...emptyFilters, organization_id: "org-1" } }); const filtersButton = screen.getByRole("button", { name: /^filters$/i }); - expect(filtersButton.querySelector(".bg-blue-500")).toBeInTheDocument(); + expect(within(filtersButton).getByTestId("active-filter-indicator")).toBeInTheDocument(); }); - it("does not show the blue dot indicator when all filters are empty", () => { + it("should not show the active filter indicator when all filters are empty", () => { renderFilters({ filters: emptyFilters }); const filtersButton = screen.getByRole("button", { name: /^filters$/i }); - expect(filtersButton.querySelector(".bg-blue-500")).not.toBeInTheDocument(); + expect(within(filtersButton).queryByTestId("active-filter-indicator")).not.toBeInTheDocument(); }); }); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/teams/components/TeamsFilters.tsx b/ui/litellm-dashboard/src/app/(dashboard)/teams/components/TeamsFilters.tsx index 3c7d0951a5e..04c65ffe268 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/teams/components/TeamsFilters.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/teams/components/TeamsFilters.tsx @@ -70,7 +70,7 @@ const TeamsFilters = ({ Filters {(filters.team_id || filters.team_alias || filters.organization_id) && ( - + )} diff --git a/ui/litellm-dashboard/src/app/(dashboard)/teams/components/TeamsTable/ModelsCell.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/teams/components/TeamsTable/ModelsCell.test.tsx index 26fc985f464..747ce518cf9 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/teams/components/TeamsTable/ModelsCell.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/teams/components/TeamsTable/ModelsCell.test.tsx @@ -1,4 +1,4 @@ -import { render, screen, fireEvent } from "@testing-library/react"; +import { act, render, screen } from "@testing-library/react"; import React from "react"; import { describe, expect, it, vi } from "vitest"; import { Team } from "@/components/key_team_helpers/key_list"; @@ -45,19 +45,19 @@ const renderModelsCell = (team: Team) => ); describe("ModelsCell", () => { - it("shows 'All Proxy Models' badge when the models array is empty", () => { + it("should show 'All Proxy Models' badge when the models array is empty", () => { renderModelsCell(makeTeam([])); expect(screen.getByText("All Proxy Models")).toBeInTheDocument(); }); - it("shows an 'All Proxy Models' badge when the model value is 'all-proxy-models'", () => { + it("should show an 'All Proxy Models' badge when the model value is 'all-proxy-models'", () => { renderModelsCell(makeTeam(["all-proxy-models"])); expect(screen.getByText("All Proxy Models")).toBeInTheDocument(); }); - it("displays individual model badges for up to 3 models without an accordion", () => { + it("should display individual model badges for up to 3 models without an accordion", () => { renderModelsCell(makeTeam(["gpt-4", "gpt-3.5-turbo", "claude-3"])); expect(screen.getByText("gpt-4")).toBeInTheDocument(); @@ -66,7 +66,7 @@ describe("ModelsCell", () => { expect(screen.queryByRole("button", { name: /accordion/i })).not.toBeInTheDocument(); }); - it("truncates model names longer than 30 characters with an ellipsis", () => { + it("should truncate model names longer than 30 characters with an ellipsis", () => { const longName = "a-very-long-model-name-exceeding-thirty-chars"; renderModelsCell(makeTeam([longName])); @@ -75,7 +75,7 @@ describe("ModelsCell", () => { expect(badge.textContent!.length).toBeLessThanOrEqual(33); // 30 chars + "..." }); - it("shows the first 3 models and a '+N more models' badge when there are more than 3 models", () => { + it("should show the first 3 models and a '+N more models' badge when there are more than 3 models", () => { renderModelsCell(makeTeam(["m1", "m2", "m3", "m4", "m5"])); expect(screen.getByText("m1")).toBeInTheDocument(); @@ -86,43 +86,51 @@ describe("ModelsCell", () => { expect(screen.queryByText("m5")).not.toBeInTheDocument(); }); - it("uses singular 'more model' when there is exactly 1 overflow model", () => { + it("should use singular 'more model' when there is exactly 1 overflow model", () => { renderModelsCell(makeTeam(["m1", "m2", "m3", "m4"])); expect(screen.getByText("+1 more model")).toBeInTheDocument(); }); - it("shows the accordion toggle button when there are more than 3 models", () => { + it("should show the accordion toggle button when there are more than 3 models", () => { renderModelsCell(makeTeam(["m1", "m2", "m3", "m4"])); expect(screen.getByRole("button", { name: /accordion/i })).toBeInTheDocument(); }); - it("expands to show all models when the accordion toggle is clicked", () => { + it("should expand to show all models when the accordion toggle is clicked", () => { renderModelsCell(makeTeam(["m1", "m2", "m3", "m4", "m5"])); - fireEvent.click(screen.getByRole("button", { name: /accordion/i })); + act(() => { + screen.getByRole("button", { name: /accordion/i }).click(); + }); expect(screen.getByText("m4")).toBeInTheDocument(); expect(screen.getByText("m5")).toBeInTheDocument(); expect(screen.queryByText("+2 more models")).not.toBeInTheDocument(); }); - it("collapses back to show the overflow badge after a second click on the toggle", () => { + it("should collapse back to show the overflow badge after a second click on the toggle", () => { renderModelsCell(makeTeam(["m1", "m2", "m3", "m4", "m5"])); const toggle = screen.getByRole("button", { name: /accordion/i }); - fireEvent.click(toggle); - fireEvent.click(toggle); + act(() => { + toggle.click(); + }); + act(() => { + toggle.click(); + }); expect(screen.queryByText("m4")).not.toBeInTheDocument(); expect(screen.getByText("+2 more models")).toBeInTheDocument(); }); - it("renders 'all-proxy-models' entries in the overflow section as 'All Proxy Models' badges", () => { + it("should render 'all-proxy-models' entries in the overflow section as 'All Proxy Models' badges", () => { renderModelsCell(makeTeam(["m1", "m2", "m3", "all-proxy-models"])); - fireEvent.click(screen.getByRole("button", { name: /accordion/i })); + act(() => { + screen.getByRole("button", { name: /accordion/i }).click(); + }); // There should now be an "All Proxy Models" badge in the expanded section expect(screen.getByText("All Proxy Models")).toBeInTheDocument(); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/teams/components/modals/DeleteTeamModal.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/teams/components/modals/DeleteTeamModal.test.tsx index 13ed1073347..1e4907dcca4 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/teams/components/modals/DeleteTeamModal.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/teams/components/modals/DeleteTeamModal.test.tsx @@ -32,7 +32,7 @@ const renderModal = (props: Partial[0]> = {}) }; describe("DeleteTeamModal", () => { - it("renders the title, team name label, and confirmation input", () => { + it("should render the title, team name label, and confirmation input", () => { renderModal(); expect(screen.getByText("Delete Team")).toBeInTheDocument(); @@ -40,27 +40,27 @@ describe("DeleteTeamModal", () => { expect(screen.getByPlaceholderText("Enter team name exactly")).toBeInTheDocument(); }); - it("renders Cancel and Force Delete buttons", () => { + it("should render Cancel and Force Delete buttons", () => { renderModal(); expect(screen.getByRole("button", { name: /^cancel$/i })).toBeInTheDocument(); expect(screen.getByRole("button", { name: /force delete/i })).toBeInTheDocument(); }); - it("does not show the warning banner when the team has no keys", () => { + it("should not show the warning banner when the team has no keys", () => { renderModal({ teams: [makeTeam({ keys: [] })] }); expect(screen.queryByText(/Warning/i)).not.toBeInTheDocument(); }); - it("shows a warning with singular 'key' when the team has exactly 1 key", () => { + it("should show a warning with singular 'key' when the team has exactly 1 key", () => { const team = makeTeam({ keys: [{ token: "tok-1" } as any] }); renderModal({ teams: [team] }); expect(screen.getByText(/This team has 1 associated key\./)).toBeInTheDocument(); }); - it("shows a warning with plural 'keys' when the team has multiple keys", () => { + it("should show a warning with plural 'keys' when the team has multiple keys", () => { const team = makeTeam({ keys: [{ token: "tok-1" } as any, { token: "tok-2" } as any, { token: "tok-3" } as any], }); @@ -69,20 +69,20 @@ describe("DeleteTeamModal", () => { expect(screen.getByText(/This team has 3 associated keys\./)).toBeInTheDocument(); }); - it("notes that associated keys will also be deleted in the warning", () => { + it("should note that associated keys will also be deleted in the warning", () => { const team = makeTeam({ keys: [{ token: "tok-1" } as any] }); renderModal({ teams: [team] }); expect(screen.getByText(/Deleting the team will also delete all associated keys/)).toBeInTheDocument(); }); - it("Force Delete button is disabled when the input is empty", () => { + it("should disable Force Delete when the input is empty", () => { renderModal(); expect(screen.getByRole("button", { name: /force delete/i })).toBeDisabled(); }); - it("Force Delete button remains disabled when the input does not exactly match the team name", async () => { + it("should keep Force Delete disabled when the input does not exactly match the team name", async () => { const user = userEvent.setup(); renderModal(); @@ -91,7 +91,7 @@ describe("DeleteTeamModal", () => { expect(screen.getByRole("button", { name: /force delete/i })).toBeDisabled(); }); - it("enables Force Delete only after typing the exact team name (case-sensitive)", async () => { + it("should enable Force Delete only after typing the exact team name (case-sensitive)", async () => { const user = userEvent.setup(); renderModal(); @@ -102,7 +102,7 @@ describe("DeleteTeamModal", () => { expect(screen.getByRole("button", { name: /force delete/i })).toBeEnabled(); }); - it("calls onConfirm when Force Delete is clicked with a valid input", async () => { + it("should call onConfirm when Force Delete is clicked with a valid input", async () => { const user = userEvent.setup(); const onConfirm = vi.fn(); renderModal({ onConfirm }); @@ -113,7 +113,7 @@ describe("DeleteTeamModal", () => { expect(onConfirm).toHaveBeenCalledTimes(1); }); - it("does not call onConfirm when Force Delete is clicked with an invalid input", async () => { + it("should not call onConfirm when Force Delete is clicked with an invalid input", async () => { const user = userEvent.setup(); const onConfirm = vi.fn(); renderModal({ onConfirm }); @@ -124,7 +124,7 @@ describe("DeleteTeamModal", () => { expect(onConfirm).not.toHaveBeenCalled(); }); - it("calls onCancel when the Cancel button is clicked", async () => { + it("should call onCancel when the Cancel button is clicked", async () => { const user = userEvent.setup(); const onCancel = vi.fn(); renderModal({ onCancel }); @@ -134,20 +134,17 @@ describe("DeleteTeamModal", () => { expect(onCancel).toHaveBeenCalledTimes(1); }); - it("calls onCancel when the X close button is clicked", async () => { + it("should call onCancel when the Close button is clicked", async () => { const user = userEvent.setup(); const onCancel = vi.fn(); renderModal({ onCancel }); - // The first button in the header is the X close button (no accessible label) - const allButtons = screen.getAllByRole("button"); - const xButton = allButtons[0]; - await user.click(xButton); + await user.click(screen.getByRole("button", { name: /^close$/i })); expect(onCancel).toHaveBeenCalledTimes(1); }); - it("resets the confirmation input when Cancel is clicked", async () => { + it("should reset the confirmation input when Cancel is clicked", async () => { const user = userEvent.setup(); renderModal(); @@ -160,15 +157,14 @@ describe("DeleteTeamModal", () => { expect(input).toHaveValue(""); }); - it("resets the confirmation input when the X close button is clicked", async () => { + it("should reset the confirmation input when the Close button is clicked", async () => { const user = userEvent.setup(); renderModal(); const input = screen.getByPlaceholderText("Enter team name exactly"); await user.type(input, "Engineering"); - const allButtons = screen.getAllByRole("button"); - await user.click(allButtons[0]); + await user.click(screen.getByRole("button", { name: /^close$/i })); expect(input).toHaveValue(""); }); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/teams/components/modals/DeleteTeamModal.tsx b/ui/litellm-dashboard/src/app/(dashboard)/teams/components/modals/DeleteTeamModal.tsx index 0be627fdfc4..28d80faacdc 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/teams/components/modals/DeleteTeamModal.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/teams/components/modals/DeleteTeamModal.tsx @@ -24,6 +24,7 @@ const DeleteTeamModal = ({ teams, teamToDelete, onCancel, onConfirm }: DeleteTea

Delete Team