Merge pull request #41773 from BerriAI/litellm_dashboard-form-happy-paths

test(ui): cover dashboard form journeys
This commit is contained in:
yuneng-jiang 2026-09-21 16:56:25 -07:00 • committed by GitHub
commit a158aa878f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 298 additions and 2 deletions

View file

@ -12,17 +12,111 @@ export async function captureRequestBody(
match: { method: string; urlIncludes: string },
action: () => Promise<void>,
): Promise<Record<string, any>> {
const pending = page.waitForRequest((req) => req.method() === match.method && req.url().includes(match.urlIncludes));
const pending = page.waitForRequest(
(req) =>
req.method() === match.method && req.url().includes(match.urlIncludes),
);
await action();
const request = await pending;
return JSON.parse(request.postData() ?? "{}") as Record<string, any>;
}
/** Reads an endpoint as the master key, so a failure is bad data and not an expired UI token. */
export async function readBack<T = any>(page: Page, endpoint: string): Promise<T> {
export async function readBack<T = any>(
page: Page,
endpoint: string,
): Promise<T> {
const res = await page.request.get(endpoint, {
headers: { Authorization: `Bearer ${masterKey()}` },
});
expect(res.ok(), `GET ${endpoint}`).toBe(true);
return (await res.json()) as T;
}
type OperationOutcome =
| { readonly status: "success" }
| { readonly status: "failure"; readonly error: unknown };
type RunFailure =
| { readonly status: "action_failure"; readonly error: unknown }
| { readonly status: "cleanup_failure"; readonly error: unknown }
| {
readonly status: "action_and_cleanup_failure";
readonly actionError: unknown;
readonly cleanupError: unknown;
};
function toRunFailure(
actionOutcome: OperationOutcome,
cleanupOutcome: OperationOutcome,
): RunFailure | null {
if (
actionOutcome.status === "failure" &&
cleanupOutcome.status === "failure"
) {
return {
status: "action_and_cleanup_failure",
actionError: actionOutcome.error,
cleanupError: cleanupOutcome.error,
};
}
if (actionOutcome.status === "failure") {
return { status: "action_failure", error: actionOutcome.error };
}
if (cleanupOutcome.status === "failure") {
return { status: "cleanup_failure", error: cleanupOutcome.error };
}
return null;
}
function raiseRunFailure(failure: RunFailure): never {
switch (failure.status) {
case "action_failure":
throw failure.error;
case "cleanup_failure":
throw failure.error;
case "action_and_cleanup_failure":
throw new AggregateError(
[failure.actionError, failure.cleanupError],
"Action and cleanup failed",
);
}
}
async function runAction(
action: () => void | Promise<void>,
): Promise<OperationOutcome> {
return Promise.resolve()
.then(action)
.then(
() => ({ status: "success" as const }),
(error: unknown) => ({ status: "failure" as const, error }),
);
}
async function runCleanup(
cleanup: () => boolean | Promise<boolean>,
): Promise<OperationOutcome> {
return Promise.resolve()
.then(cleanup)
.then(
(succeeded) =>
succeeded
? { status: "success" as const }
: {
status: "failure" as const,
error: new Error("Failed to clean up UI E2E resource"),
},
(error: unknown) => ({ status: "failure" as const, error }),
);
}
export async function runWithCleanup(
action: () => void | Promise<void>,
cleanup: () => boolean | Promise<boolean>,
): Promise<void> {
const actionOutcome = await runAction(action);
const cleanupOutcome = await runCleanup(cleanup);
const failure = toRunFailure(actionOutcome, cleanupOutcome);
if (failure !== null) raiseRunFailure(failure);
}

View file

@ -0,0 +1,75 @@
import { test, expect } from "@playwright/test";
import { ADMIN_STORAGE_PATH } from "../../constants";
import { Page as DashboardPage } from "../../fixtures/pages";
import { navigateToPage } from "../../helpers/navigation";
import { runWithCleanup } from "../../helpers/roundTrip";
import { masterKey, uniqueSuffix } from "../../helpers/traffic";
test.use({ storageState: ADMIN_STORAGE_PATH });
test.describe("Prompt upload form", () => {
test("uploads a prompt file and reads the created prompt back", async ({
page,
}) => {
const promptId = `e2e-prompt-${uniqueSuffix()}`;
const promptContent = "Hello {{name}}";
await runWithCleanup(
async () => {
await navigateToPage(page, DashboardPage.Prompts);
await page.getByRole("button", { name: "Upload .prompt File" }).click();
await expect(
page.getByRole("dialog", { name: "Add New Prompt" }),
).toBeVisible();
await page.getByLabel("Prompt ID").fill(promptId);
await page.locator('input[type="file"]').setInputFiles({
name: "e2e.prompt",
mimeType: "text/plain",
buffer: Buffer.from(
`---\nmodel: fake-openai-gpt-4\n---\n${promptContent}\n`,
),
});
await expect(page.getByText("Selected: e2e.prompt")).toBeVisible();
await page.getByRole("button", { name: "Create Prompt" }).click();
await expect
.poll(async () => {
const response = await page.request.get(
`/prompts/${encodeURIComponent(promptId)}/info?environment=development`,
{
headers: { Authorization: `Bearer ${masterKey()}` },
},
);
return response.ok();
})
.toBe(true);
await expect
.poll(async () => {
const response = await page.request.get(
`/prompts/${encodeURIComponent(promptId)}/info?environment=development`,
{
headers: { Authorization: `Bearer ${masterKey()}` },
},
);
if (!response.ok()) return undefined;
const promptInfo = (await response.json()) as {
raw_prompt_template?: { content?: string };
};
return promptInfo.raw_prompt_template?.content;
})
.toBe(promptContent);
await expect(page.getByText(promptId, { exact: true })).toBeVisible();
},
async () => {
const response = await page.request.delete(
`/prompts/${encodeURIComponent(promptId)}?environment=development`,
{
headers: { Authorization: `Bearer ${masterKey()}` },
},
);
return response.ok();
},
);
});
});

View file

@ -0,0 +1,84 @@
import { test, expect } from "@playwright/test";
import { ADMIN_STORAGE_PATH } from "../../constants";
import { Page as DashboardPage } from "../../fixtures/pages";
import { navigateToPage } from "../../helpers/navigation";
import {
captureRequestBody,
readBack,
runWithCleanup,
} from "../../helpers/roundTrip";
import { masterKey, uniqueSuffix } from "../../helpers/traffic";
test.use({ storageState: ADMIN_STORAGE_PATH });
test.describe("Tag management", () => {
test("creates, edits, reopens, and reads back a tag", async ({ page }) => {
const tagName = `e2e-tag-${uniqueSuffix()}`;
const description = "synthetic tag description";
const updatedDescription = `${description} updated`;
await runWithCleanup(
async () => {
await navigateToPage(page, DashboardPage.TagManagement);
await page.getByRole("button", { name: "+ Create New Tag" }).click();
await expect(
page.getByRole("dialog", { name: "Create New Tag" }),
).toBeVisible();
await page.getByLabel("Tag Name").fill(tagName);
await page.getByLabel("Description").fill(description);
await page.getByRole("button", { name: "Create Tag" }).click();
await expect
.poll(async () => {
const response = await readBack<Array<{ name: string }>>(
page,
"/tag/list",
);
return response.some((tag) => tag.name === tagName);
})
.toBe(true);
await expect(page.getByText(tagName, { exact: true })).toBeVisible();
await page.getByText(tagName, { exact: true }).click();
await expect(page.getByText("Tag Name:")).toBeVisible();
await page.getByRole("button", { name: "Edit Tag" }).click();
await page.getByLabel("Description").fill(updatedDescription);
const updateBody = await captureRequestBody(
page,
{ method: "POST", urlIncludes: "/tag/update" },
() => page.getByRole("button", { name: "Save Changes" }).click(),
);
expect(updateBody).toMatchObject({
name: tagName,
description: updatedDescription,
});
await expect
.poll(async () => {
const infoResponse = await page.request.post("/tag/info", {
headers: { Authorization: `Bearer ${masterKey()}` },
data: { names: [tagName] },
});
expect(infoResponse.ok()).toBe(true);
const info = (await infoResponse.json()) as Record<
string,
{ description?: string }
>;
return info[tagName]?.description;
})
.toBe(updatedDescription);
},
async () => {
const response = await page.request.post("/tag/delete", {
headers: {
Authorization: `Bearer ${masterKey()}`,
"Content-Type": "application/json",
},
data: { name: tagName },
});
return response.ok();
},
);
});
});

View file

@ -111,4 +111,36 @@ describe("SearchSelect", () => {
expect(screen.queryByText("Growth")).not.toBeInTheDocument();
expect(onValueChange).not.toHaveBeenCalled();
});
it("supports keyboard select, clear, and reselect", async () => {
const onValueChange = vi.fn();
const user = userEvent.setup();
function Controlled() {
const [value, setValue] = useState<string | null>(null);
return (
<SearchSelect
options={OPTIONS}
value={value}
onValueChange={(next) => {
setValue(next);
onValueChange(next);
}}
/>
);
}
render(<Controlled />);
const input = screen.getByRole("combobox");
await user.tab();
await user.keyboard("{Enter}");
await user.keyboard("{ArrowDown}{Enter}");
expect(onValueChange).toHaveBeenLastCalledWith("team-1");
const clear = screen.getByRole("button", { name: "Clear" });
clear.focus();
await user.keyboard("{Enter}");
expect(onValueChange).toHaveBeenLastCalledWith(null);
input.focus();
await user.keyboard("{Enter}{ArrowDown}{Enter}");
expect(onValueChange).toHaveBeenLastCalledWith("team-1");
});
});

View file

@ -344,4 +344,15 @@ describe("RequestLogsFilters", () => {
expect(set).toHaveBeenCalledWith(LOG_FILTER_IDS.CACHE_STATUS, undefined);
});
it("clears the raw Error Code combobox through the undefined filter contract", async () => {
const user = userEvent.setup();
const { set } = renderFilters({ [LOG_FILTER_IDS.ERROR_CODE]: "429" });
const input = await screen.findByPlaceholderText("Select or type an error code");
await user.click(input);
await user.click(screen.getByRole("button", { name: "Clear", hidden: true }));
expect(set).toHaveBeenCalledWith(LOG_FILTER_IDS.ERROR_CODE, undefined);
});
});