Skip to content

Commit 9cee51a

Browse files
authored
Merge pull request BerriAI#21004 from BerriAI/litellm_ui_auto_router
[Fix] UI - Add Auto Router: Description Text Input Focus
2 parents c867740 + af6ff69 commit 9cee51a

File tree

5 files changed

+575
-283
lines changed

5 files changed

+575
-283
lines changed
Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
import { render, screen, waitFor, fireEvent } from "@testing-library/react";
2+
import userEvent from "@testing-library/user-event";
3+
import { describe, expect, it, vi } from "vitest";
4+
import RouterConfigBuilder from "./RouterConfigBuilder";
5+
6+
const MOCK_MODEL_INFO = [
7+
{ model_group: "gpt-4", mode: "chat" },
8+
{ model_group: "gpt-3.5-turbo", mode: "chat" },
9+
{ model_group: "claude-3-opus", mode: "chat" },
10+
];
11+
12+
describe("RouterConfigBuilder", () => {
13+
it("should render", () => {
14+
render(<RouterConfigBuilder modelInfo={MOCK_MODEL_INFO} />);
15+
16+
expect(screen.getByText("Routes Configuration")).toBeInTheDocument();
17+
});
18+
19+
it("should display Add Route button", () => {
20+
render(<RouterConfigBuilder modelInfo={MOCK_MODEL_INFO} />);
21+
22+
expect(screen.getByRole("button", { name: /add route/i })).toBeInTheDocument();
23+
});
24+
25+
it("should show empty state when no routes are configured", () => {
26+
render(<RouterConfigBuilder modelInfo={MOCK_MODEL_INFO} />);
27+
28+
expect(screen.getByText(/no routes configured/i)).toBeInTheDocument();
29+
});
30+
31+
it("should add a route when Add Route is clicked", async () => {
32+
const user = userEvent.setup();
33+
render(<RouterConfigBuilder modelInfo={MOCK_MODEL_INFO} />);
34+
35+
await user.click(screen.getByRole("button", { name: /add route/i }));
36+
37+
expect(screen.getByText("Route 1: Unnamed")).toBeInTheDocument();
38+
});
39+
40+
it("should call onChange when a route is added", async () => {
41+
const user = userEvent.setup();
42+
const onChange = vi.fn();
43+
render(<RouterConfigBuilder modelInfo={MOCK_MODEL_INFO} onChange={onChange} />);
44+
45+
await user.click(screen.getByRole("button", { name: /add route/i }));
46+
47+
expect(onChange).toHaveBeenCalledWith({
48+
routes: [
49+
expect.objectContaining({
50+
name: "",
51+
utterances: [],
52+
description: "",
53+
score_threshold: 0.5,
54+
}),
55+
],
56+
});
57+
});
58+
59+
it("should initialize routes from value prop", async () => {
60+
const value = {
61+
routes: [
62+
{
63+
name: "gpt-4",
64+
utterances: ["hello", "hi"],
65+
description: "For greetings",
66+
score_threshold: 0.7,
67+
},
68+
],
69+
};
70+
render(<RouterConfigBuilder modelInfo={MOCK_MODEL_INFO} value={value} />);
71+
72+
await waitFor(() => {
73+
expect(screen.getByText("Route 1: gpt-4")).toBeInTheDocument();
74+
});
75+
});
76+
77+
it("should support both name and model fields in value prop", async () => {
78+
const value = {
79+
routes: [{ model: "gpt-3.5-turbo", utterances: [], description: "", score_threshold: 0.5 }],
80+
};
81+
render(<RouterConfigBuilder modelInfo={MOCK_MODEL_INFO} value={value} />);
82+
83+
await waitFor(() => {
84+
expect(screen.getByText("Route 1: gpt-3.5-turbo")).toBeInTheDocument();
85+
});
86+
});
87+
88+
it("should remove a route when delete button is clicked", async () => {
89+
const user = userEvent.setup();
90+
const value = {
91+
routes: [
92+
{
93+
name: "gpt-4",
94+
utterances: [],
95+
description: "",
96+
score_threshold: 0.5,
97+
},
98+
],
99+
};
100+
render(<RouterConfigBuilder modelInfo={MOCK_MODEL_INFO} value={value} />);
101+
102+
await waitFor(() => {
103+
expect(screen.getByText("Route 1: gpt-4")).toBeInTheDocument();
104+
});
105+
106+
const deleteButton = screen.getByRole("button", { name: "delete" });
107+
await user.click(deleteButton);
108+
109+
await waitFor(() => {
110+
expect(screen.queryByText("Route 1: gpt-4")).not.toBeInTheDocument();
111+
expect(screen.getByText(/no routes configured/i)).toBeInTheDocument();
112+
});
113+
});
114+
115+
it("should call onChange when route is removed", async () => {
116+
const user = userEvent.setup();
117+
const onChange = vi.fn();
118+
const value = {
119+
routes: [
120+
{
121+
name: "gpt-4",
122+
utterances: [],
123+
description: "",
124+
score_threshold: 0.5,
125+
},
126+
],
127+
};
128+
render(<RouterConfigBuilder modelInfo={MOCK_MODEL_INFO} value={value} onChange={onChange} />);
129+
130+
await waitFor(() => {
131+
expect(screen.getByText("Route 1: gpt-4")).toBeInTheDocument();
132+
});
133+
134+
const deleteButton = screen.getByRole("button", { name: "delete" });
135+
await user.click(deleteButton);
136+
137+
await waitFor(() => {
138+
expect(onChange).toHaveBeenCalledWith({ routes: [] });
139+
});
140+
});
141+
142+
143+
144+
145+
it("should update route when description is changed", async () => {
146+
const user = userEvent.setup();
147+
const onChange = vi.fn();
148+
const value = {
149+
routes: [
150+
{
151+
name: "gpt-4",
152+
utterances: [],
153+
description: "",
154+
score_threshold: 0.5,
155+
},
156+
],
157+
};
158+
render(<RouterConfigBuilder modelInfo={MOCK_MODEL_INFO} value={value} onChange={onChange} />);
159+
160+
await waitFor(() => {
161+
expect(screen.getByText("Route 1: gpt-4")).toBeInTheDocument();
162+
});
163+
164+
const descriptionInput = screen.getByPlaceholderText("Describe when this route should be used...");
165+
await user.type(descriptionInput, "For code generation");
166+
167+
await waitFor(() => {
168+
const lastCall = onChange.mock.calls[onChange.mock.calls.length - 1];
169+
expect(lastCall[0].routes[0].description).toBe("For code generation");
170+
});
171+
});
172+
173+
it("should update route when score threshold is changed", async () => {
174+
const onChange = vi.fn();
175+
const value = {
176+
routes: [
177+
{
178+
name: "gpt-4",
179+
utterances: [],
180+
description: "",
181+
score_threshold: 0.5,
182+
},
183+
],
184+
};
185+
render(<RouterConfigBuilder modelInfo={MOCK_MODEL_INFO} value={value} onChange={onChange} />);
186+
187+
await waitFor(() => {
188+
expect(screen.getByText("Route 1: gpt-4")).toBeInTheDocument();
189+
});
190+
191+
const scoreInput = screen.getByRole("spinbutton");
192+
fireEvent.change(scoreInput, { target: { value: "0.9" } });
193+
194+
await waitFor(() => {
195+
const lastCall = onChange.mock.calls[onChange.mock.calls.length - 1];
196+
expect(lastCall[0].routes[0].score_threshold).toBe(0.9);
197+
});
198+
});
199+
200+
it("should add multiple routes", async () => {
201+
const user = userEvent.setup();
202+
render(<RouterConfigBuilder modelInfo={MOCK_MODEL_INFO} />);
203+
204+
await user.click(screen.getByRole("button", { name: /add route/i }));
205+
await user.click(screen.getByRole("button", { name: /add route/i }));
206+
207+
expect(screen.getByText("Route 1: Unnamed")).toBeInTheDocument();
208+
expect(screen.getByText("Route 2: Unnamed")).toBeInTheDocument();
209+
});
210+
211+
it("should toggle JSON preview visibility", async () => {
212+
const user = userEvent.setup();
213+
const { container } = render(<RouterConfigBuilder modelInfo={MOCK_MODEL_INFO} />);
214+
215+
expect(screen.getByText("JSON Preview")).toBeInTheDocument();
216+
expect(screen.getByRole("button", { name: "Show" })).toBeInTheDocument();
217+
expect(container.querySelector("pre")).not.toBeInTheDocument();
218+
219+
await user.click(screen.getByRole("button", { name: "Show" }));
220+
221+
expect(screen.getByRole("button", { name: "Hide" })).toBeInTheDocument();
222+
expect(container.querySelector("pre")).toBeInTheDocument();
223+
224+
await user.click(screen.getByRole("button", { name: "Hide" }));
225+
226+
expect(screen.getByRole("button", { name: "Show" })).toBeInTheDocument();
227+
expect(container.querySelector("pre")).not.toBeInTheDocument();
228+
});
229+
230+
it("should display JSON preview with route data when routes exist", async () => {
231+
const user = userEvent.setup();
232+
const { container } = render(
233+
<RouterConfigBuilder
234+
modelInfo={MOCK_MODEL_INFO}
235+
value={{
236+
routes: [
237+
{ name: "gpt-4", utterances: ["hello"], description: "test", score_threshold: 0.8 },
238+
],
239+
}}
240+
/>,
241+
);
242+
243+
await waitFor(() => {
244+
expect(screen.getByText("Route 1: gpt-4")).toBeInTheDocument();
245+
});
246+
247+
await user.click(screen.getByRole("button", { name: "Show" }));
248+
249+
const preElement = container.querySelector("pre");
250+
expect(preElement).toBeInTheDocument();
251+
expect(preElement?.textContent).toContain("gpt-4");
252+
expect(preElement?.textContent).toContain("hello");
253+
expect(preElement?.textContent).toContain("0.8");
254+
});
255+
256+
it("should display model selector with options from modelInfo", async () => {
257+
const value = {
258+
routes: [
259+
{ name: "", utterances: [], description: "", score_threshold: 0.5 },
260+
],
261+
};
262+
render(<RouterConfigBuilder modelInfo={MOCK_MODEL_INFO} value={value} />);
263+
264+
await waitFor(() => {
265+
expect(screen.getByText("Route 1: Unnamed")).toBeInTheDocument();
266+
});
267+
268+
expect(screen.getByText("Model")).toBeInTheDocument();
269+
const comboboxes = screen.getAllByRole("combobox");
270+
expect(comboboxes.length).toBeGreaterThan(0);
271+
});
272+
273+
it("should clear routes when value prop changes to empty", async () => {
274+
const value = {
275+
routes: [
276+
{
277+
name: "gpt-4",
278+
utterances: [],
279+
description: "",
280+
score_threshold: 0.5,
281+
},
282+
],
283+
};
284+
const { rerender } = render(<RouterConfigBuilder modelInfo={MOCK_MODEL_INFO} value={value} />);
285+
286+
await waitFor(() => {
287+
expect(screen.getByText("Route 1: gpt-4")).toBeInTheDocument();
288+
});
289+
290+
rerender(<RouterConfigBuilder modelInfo={MOCK_MODEL_INFO} value={{ routes: [] }} />);
291+
292+
await waitFor(() => {
293+
expect(screen.getByText(/no routes configured/i)).toBeInTheDocument();
294+
});
295+
});
296+
});

0 commit comments

Comments
 (0)