[Test] UI: Add unit tests for 10 untested components#23891
[Test] UI: Add unit tests for 10 untested components#23891yuneng-jiang merged 1 commit intolitellm_yj_march_17_2026from
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Greptile SummaryThis PR adds 67 Vitest + React Testing Library unit tests across 10 previously untested UI components, providing meaningful first-coverage for Key observations:
Confidence Score: 4/5
|
| Filename | Overview |
|---|---|
| ui/litellm-dashboard/src/components/ToolPolicies/PolicySelect.test.tsx | New test file for PolicySelect; aria-expanded="false" in the disabled test does not verify disabled state (aria-disabled would be correct), and policyStyle assertions use fragile index-based comparisons rather than value-based matching. |
| ui/litellm-dashboard/src/components/common_components/RateLimitTypeFormItem.test.tsx | New tests covering TPM/RPM labels, placeholder, and onChange; "should render" and "should display TPM label for tpm type" are exact duplicates providing no additional coverage. |
| ui/litellm-dashboard/src/components/agents/agent_card_grid.test.tsx | New tests for AgentCardGrid with mock of AgentCard; correctly validates loading skeletons, empty states, and click handlers. Dynamic userEvent import inside the test body has been flagged in previous review threads. |
| ui/litellm-dashboard/src/components/EntityUsageExport/ExportSummary.test.tsx | New tests for ExportSummary; locale-sensitive toLocaleDateString() usage in regex (already noted in previous review threads) could cause flaky behaviour across environments. |
| ui/litellm-dashboard/src/components/HelpLink.test.tsx | Extended with 5 new tests covering href attribute, initial tooltip/menu hidden state, and learn-more link; all look correct and well-structured. |
| ui/litellm-dashboard/src/components/DebugWarningBanner.test.tsx | New tests correctly mock useHealthReadiness hook and verify conditional rendering; all five tests are distinct and meaningful. |
| ui/litellm-dashboard/src/components/EntityUsageExport/ExportFormatSelector.test.tsx | New tests for ExportFormatSelector covering label rendering and CSV/JSON display; straightforward and correct. |
| ui/litellm-dashboard/src/components/EntityUsageExport/ExportTypeSelector.test.tsx | New tests validating entity type interpolation, radio selection, and onChange delegation; implementation matches ExportTypeSelector correctly. |
| ui/litellm-dashboard/src/components/GuardrailsMonitor/MetricCard.test.tsx | New tests for MetricCard covering label, value, optional icon, and subtitle; all tests are distinct and appropriately scoped. |
| ui/litellm-dashboard/src/components/add_model/ComplexityRouterConfig.test.tsx | New tests covering tier labels, example queries, and score thresholds in ComplexityRouterConfig; well-structured and comprehensive. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
TU[Test Utilities\nrenderWithProviders / screen]
subgraph New Test Files
HLT[HelpLink.test.tsx\n+5 tests]
DWB[DebugWarningBanner.test.tsx\n5 tests]
EFS[ExportFormatSelector.test.tsx\n3 tests]
ETS[ExportTypeSelector.test.tsx\n5 tests]
ESM[ExportSummary.test.tsx\n5 tests]
MC[MetricCard.test.tsx\n7 tests]
PS[PolicySelect.test.tsx\n9 tests]
CRC[ComplexityRouterConfig.test.tsx\n5 tests]
RL[RateLimitTypeFormItem.test.tsx\n5 tests]
ACG[agent_card_grid.test.tsx\n6 tests]
end
subgraph Mocked Dependencies
UHR[useHealthReadiness hook]
AC[agent_card component]
end
TU --> HLT & DWB & EFS & ETS & ESM & MC & PS & CRC & RL & ACG
DWB -- vi.mock --> UHR
ACG -- vi.mock --> AC
PS -- ⚠️ aria-expanded assertion\ndoes not verify disabled --> PS
PS -- ⚠️ index-based policyStyle\nassertions are fragile --> PS
RL -- ⚠️ duplicate render /\nTPM label test cases --> RL
Last reviewed commit: e0e1ac6
| <AgentCardGrid {...defaultProps} onAgentClick={onAgentClick} /> | ||
| ); | ||
| const { default: userEvent } = await import("@testing-library/user-event"); | ||
| const user = userEvent.setup(); | ||
| await user.click(screen.getByTestId("agent-card-agent-1")); | ||
| expect(onAgentClick).toHaveBeenCalledWith("agent-1"); |
There was a problem hiding this comment.
Inconsistent dynamic import of
userEvent
userEvent is statically imported at the top of every other test file in this PR (e.g. ExportTypeSelector.test.tsx, HelpLink.test.tsx), but here it is dynamically imported inside the test body. This introduces unnecessary async overhead and is inconsistent with the established pattern. Move the import to the top of the file alongside the other imports.
| <AgentCardGrid {...defaultProps} onAgentClick={onAgentClick} /> | |
| ); | |
| const { default: userEvent } = await import("@testing-library/user-event"); | |
| const user = userEvent.setup(); | |
| await user.click(screen.getByTestId("agent-card-agent-1")); | |
| expect(onAgentClick).toHaveBeenCalledWith("agent-1"); | |
| it("should call onAgentClick when a card is clicked", async () => { | |
| const onAgentClick = vi.fn(); | |
| renderWithProviders( | |
| <AgentCardGrid {...defaultProps} onAgentClick={onAgentClick} /> | |
| ); | |
| const user = userEvent.setup(); | |
| await user.click(screen.getByTestId("agent-card-agent-1")); | |
| expect(onAgentClick).toHaveBeenCalledWith("agent-1"); | |
| }); |
And add at the top of the file:
import userEvent from "@testing-library/user-event";Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| describe("PolicySelect", () => { | ||
| it("should render", () => { | ||
| renderWithProviders( | ||
| <PolicySelect | ||
| value="untrusted" | ||
| toolName="test-tool" | ||
| saving={false} | ||
| onChange={vi.fn()} | ||
| /> | ||
| ); | ||
| expect(screen.getByText("untrusted")).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it("should show the current policy value", () => { | ||
| renderWithProviders( | ||
| <PolicySelect | ||
| value="trusted" | ||
| toolName="test-tool" | ||
| saving={false} | ||
| onChange={vi.fn()} | ||
| /> |
There was a problem hiding this comment.
PolicySelect.onChange two-argument signature is never exercised
The component's onChange prop has the signature (toolName: string, policy: string) => void — it is called as onChange(toolName, v) in the implementation. However, none of the tests verify that onChange is actually invoked with both arguments. This means a refactor that accidentally drops toolName from the call would go undetected. Consider adding a test like:
it("should call onChange with toolName and new policy value when selection changes", async () => {
const user = userEvent.setup();
const onChange = vi.fn();
renderWithProviders(
<PolicySelect
value="untrusted"
toolName="test-tool"
saving={false}
onChange={onChange}
/>
);
await user.click(screen.getByRole("combobox"));
await user.click(screen.getByText("trusted"));
expect(onChange).toHaveBeenCalledWith("test-tool", "trusted");
});| const dateRange = { from, to }; | ||
| renderWithProviders( | ||
| <ExportSummary dateRange={dateRange} selectedFilters={[]} /> | ||
| ); | ||
| expect(screen.getByText(new RegExp(from.toLocaleDateString()))).toBeInTheDocument(); | ||
| expect(screen.getByText(new RegExp(to.toLocaleDateString()))).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it("should show filter count when filters are selected", () => { |
There was a problem hiding this comment.
Locale-sensitive
toLocaleDateString() used in regex constructor
new RegExp(from.toLocaleDateString()) is fragile in two ways:
- In some locales (e.g. German
"1.1.2024"), the.in the formatted string becomes a wildcard in the resulting regex, so the assertion matches more broadly than intended. - If the test is run in a locale that formats the date differently than the CI environment, the regex could silently never fail.
Both issues can be avoided by using a fixed locale:
| const dateRange = { from, to }; | |
| renderWithProviders( | |
| <ExportSummary dateRange={dateRange} selectedFilters={[]} /> | |
| ); | |
| expect(screen.getByText(new RegExp(from.toLocaleDateString()))).toBeInTheDocument(); | |
| expect(screen.getByText(new RegExp(to.toLocaleDateString()))).toBeInTheDocument(); | |
| }); | |
| it("should show filter count when filters are selected", () => { | |
| expect(screen.getByText(new RegExp(from.toLocaleDateString("en-US")))).toBeInTheDocument(); | |
| expect(screen.getByText(new RegExp(to.toLocaleDateString("en-US")))).toBeInTheDocument(); |
And create the dates with the same locale in mind, or alternatively use toBeInTheDocument with the exact rendered string.
| it("should use custom learn more text when provided", async () => { | ||
| const user = userEvent.setup(); | ||
| renderWithProviders( | ||
| <HelpIcon | ||
| content="Help text" | ||
| learnMoreHref="https://docs.example.com" | ||
| learnMoreText="View docs" |
There was a problem hiding this comment.
Tooltip "Learn more" link is inaccessible to mouse users — test doesn't expose this
In HelpLink.tsx, the tooltip container has style={{ pointerEvents: "none" }}. The button's onMouseLeave fires as soon as the cursor moves away from the button (e.g. to reach the "Learn more" link), which immediately sets showTooltip = false and unmounts the tooltip. The link therefore cannot be clicked by a mouse user even though it is rendered.
The test only asserts the link text appears immediately after hover — it doesn't attempt to move focus to the link and click it — so the inaccessibility is not caught. Consider adding an interaction test, and fixing the component to keep the tooltip visible while the mouse is over it (e.g. include both the button and the tooltip in the onMouseLeave boundary).
Add Vitest + RTL tests for HelpLink, DebugWarningBanner, ExportFormatSelector, ExportTypeSelector, ExportSummary, MetricCard, PolicySelect, ComplexityRouterConfig, RateLimitTypeFormItem, and AgentCardGrid (67 tests total). Co-Authored-By: Claude Opus 4.6 <[email protected]>
0ca92d6 to
e0e1ac6
Compare
251c279
into
litellm_yj_march_17_2026
| expect(screen.getByRole("combobox")).toHaveAttribute("aria-expanded", "false"); | ||
| expect(screen.getByRole("combobox").closest(".ant-select")).toHaveClass("ant-select-disabled"); |
There was a problem hiding this comment.
aria-expanded does not verify disabled state
aria-expanded="false" simply means the dropdown is currently closed — which is also true for a non-disabled select that hasn't been opened. This assertion passes regardless of the saving prop and therefore doesn't contribute to verifying the disabled state. A non-disabled select would also satisfy this assertion, so it will not catch a regression where disabled={saving} is accidentally removed.
The correct ARIA attribute for disabled state on an Ant Design combobox is aria-disabled="true". Replace the first assertion:
| expect(screen.getByRole("combobox")).toHaveAttribute("aria-expanded", "false"); | |
| expect(screen.getByRole("combobox").closest(".ant-select")).toHaveClass("ant-select-disabled"); | |
| expect(screen.getByRole("combobox")).toHaveAttribute("aria-disabled", "true"); | |
| expect(screen.getByRole("combobox").closest(".ant-select")).toHaveClass("ant-select-disabled"); |
| it("should render", () => { | ||
| renderWithProviders( | ||
| <Wrapper> | ||
| <RateLimitTypeFormItem type="tpm" name="tpm_type" /> | ||
| </Wrapper> | ||
| ); | ||
| expect(screen.getByText(/TPM Rate Limit Type/)).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it("should display TPM label for tpm type", () => { | ||
| renderWithProviders( | ||
| <Wrapper> | ||
| <RateLimitTypeFormItem type="tpm" name="tpm_type" /> | ||
| </Wrapper> | ||
| ); | ||
| expect(screen.getByText(/TPM Rate Limit Type/)).toBeInTheDocument(); |
There was a problem hiding this comment.
Duplicate test cases reduce suite value
"should render" (lines 13–20) and "should display TPM label for tpm type" (lines 22–29) are identical: both render <RateLimitTypeFormItem type="tpm" name="tpm_type" /> and assert screen.getByText(/TPM Rate Limit Type/). If the first test passes, the second must too, and vice versa — one of them provides no additional coverage. Consider merging them into a single meaningful test, or converting "should render" into a smoke test that verifies the component mounts without throwing.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| it("should return the matching option for a known policy", () => { | ||
| expect(policyStyle("trusted")).toEqual(INPUT_POLICY_OPTIONS[1]); | ||
| }); | ||
|
|
||
| it("should return the matching option for blocked", () => { | ||
| expect(policyStyle("blocked")).toEqual(INPUT_POLICY_OPTIONS[2]); | ||
| }); | ||
|
|
||
| it("should return the first option as fallback for unknown policy", () => { | ||
| expect(policyStyle("unknown")).toEqual(INPUT_POLICY_OPTIONS[0]); | ||
| }); |
There was a problem hiding this comment.
policyStyle tests rely on fragile index-based assertions
Both assertions reference positional indices into INPUT_POLICY_OPTIONS:
expect(policyStyle("trusted")).toEqual(INPUT_POLICY_OPTIONS[1]);
expect(policyStyle("blocked")).toEqual(INPUT_POLICY_OPTIONS[2]);If the order of INPUT_POLICY_OPTIONS is ever changed (e.g., to alphabetical), policyStyle("trusted") will still return the object whose value === "trusted", but INPUT_POLICY_OPTIONS[1] may now be a different object — causing a spurious test failure even though the function behaves correctly. Assert on the return value's identity property instead:
| it("should return the matching option for a known policy", () => { | |
| expect(policyStyle("trusted")).toEqual(INPUT_POLICY_OPTIONS[1]); | |
| }); | |
| it("should return the matching option for blocked", () => { | |
| expect(policyStyle("blocked")).toEqual(INPUT_POLICY_OPTIONS[2]); | |
| }); | |
| it("should return the first option as fallback for unknown policy", () => { | |
| expect(policyStyle("unknown")).toEqual(INPUT_POLICY_OPTIONS[0]); | |
| }); | |
| it("should return the matching option for a known policy", () => { | |
| expect(policyStyle("trusted")).toMatchObject({ value: "trusted" }); | |
| }); | |
| it("should return the matching option for blocked", () => { | |
| expect(policyStyle("blocked")).toMatchObject({ value: "blocked" }); | |
| }); |
Summary
10 UI component files had no test coverage. This PR adds Vitest + React Testing Library tests for each, totaling 67 new tests.
Testing
All 67 tests pass locally via
npx vitest run.Components tested:
useHealthReadinesshookType
✅ Test