mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-24 00:52:24 +00:00
KeyValueInput and QueryParamInput each seeded a private copy of their rows from the value prop with a one-time useState initializer. The antd form they were written for hid that: rc-field-form bumps an internal resetCount key on resetFields, which remounts a Field's children, so the private copy was thrown away on every reset. react-hook-form's reset does not remount, and the modal is hidden rather than unmounted, so after Cancel the rows stayed on screen holding the old values while the form value went back to empty. The visible cost was a blocked create flow. A leaked header row made the modal look configured, but the form value behind it was gone, so submitting a fresh path and target was refused with "Please configure the headers" and no request was sent. Typing one character into the leaked row put a value back and the submit went through, which is not something a user can guess. Both inputs are now controlled off the value prop, which is an array of pairs rather than a record. A record cannot represent a row whose name is still empty, which is the reason the private copy existed: two blank rows collapse into one and a half-typed row disappears as it is typed. With pairs the field value is the editable shape, the second source of truth is gone, and a form reset clears the rows like every other field. add_pass_through converts to a record at submit, so the request payload is unchanged. Headers now require at least one row with a non-empty name. Previously that was enforced by accident, because adding a row did not notify the form at all.
60 lines
2.4 KiB
TypeScript
60 lines
2.4 KiB
TypeScript
import { useState } from "react";
|
|
import { fireEvent, render, screen } from "@testing-library/react";
|
|
import userEvent from "@testing-library/user-event";
|
|
import { describe, expect, it, vi } from "vitest";
|
|
|
|
import QueryParamInput from "./query_param_input";
|
|
import type { KeyValuePair } from "./key_value_input";
|
|
|
|
const ControlledQueryParamInput = ({ onChange }: { onChange?: (value: readonly KeyValuePair[]) => void }) => {
|
|
const [value, setValue] = useState<readonly KeyValuePair[]>([]);
|
|
return (
|
|
<QueryParamInput
|
|
value={value}
|
|
onChange={(next) => {
|
|
setValue(next);
|
|
onChange?.(next);
|
|
}}
|
|
/>
|
|
);
|
|
};
|
|
|
|
describe("QueryParamInput", () => {
|
|
it("renders existing query parameter pairs", () => {
|
|
render(<QueryParamInput value={[["version", "v1"]]} />);
|
|
|
|
expect(screen.getByPlaceholderText("Parameter Name (e.g., version)")).toHaveValue("version");
|
|
expect(screen.getByPlaceholderText("Parameter Value (e.g., v1)")).toHaveValue("v1");
|
|
});
|
|
|
|
it("adds a query parameter row and emits edits", async () => {
|
|
const user = userEvent.setup();
|
|
const onChange = vi.fn();
|
|
render(<ControlledQueryParamInput onChange={onChange} />);
|
|
|
|
await user.click(screen.getByRole("button", { name: /Add Query Parameter$/ }));
|
|
fireEvent.change(screen.getByPlaceholderText("Parameter Name (e.g., version)"), { target: { value: "region" } });
|
|
fireEvent.change(screen.getByPlaceholderText("Parameter Value (e.g., v1)"), { target: { value: "us-west" } });
|
|
|
|
expect(onChange).toHaveBeenLastCalledWith([["region", "us-west"]]);
|
|
});
|
|
|
|
it("renders no rows once the value prop goes back to empty", () => {
|
|
const { rerender } = render(<QueryParamInput value={[["version", "v1"]]} />);
|
|
expect(screen.getByPlaceholderText("Parameter Name (e.g., version)")).toHaveValue("version");
|
|
|
|
rerender(<QueryParamInput value={[]} />);
|
|
|
|
expect(screen.queryByPlaceholderText("Parameter Name (e.g., version)")).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("keeps a half-typed row editable while its name is still empty", async () => {
|
|
const user = userEvent.setup();
|
|
render(<ControlledQueryParamInput />);
|
|
|
|
await user.click(screen.getByRole("button", { name: /Add Query Parameter$/ }));
|
|
await user.click(screen.getByRole("button", { name: /Add Query Parameter$/ }));
|
|
|
|
expect(screen.getAllByPlaceholderText("Parameter Name (e.g., version)")).toHaveLength(2);
|
|
});
|
|
});
|