litellm/ui/litellm-dashboard/e2e_tests/tests/migration/migratedPages.spec.ts
ryan-crabbe-berri 32bdd004bd
feat(ui): migrate api-keys landing to App Router path route (#30699)
Cut the default "Virtual Keys" landing (?page=api-keys) over to a path route at
(dashboard)/api-keys. The dashboard is extracted into a shared ApiKeysDashboard
component used by both the new route and the index's inline render, so there's
no duplication. Adding the MIGRATED_PAGES entry repoints the sidebar item and
redirects ?page=api-keys to /ui/api-keys.

The index is the post-login landing and still hosts the legacy switch for the
not-yet-migrated pages (models, pass-through, usage) plus the invitation flow,
so it stays. The auto-redirect now fires only for an explicit ?page= param,
leaving the bare /ui/ landing to render inline; this keeps the return-URL
handling and the invitation_id flow (both of which run at the bare landing)
intact, where a blanket redirect would have dropped them. The new route uses
useAuthorized for the login gate, matching every other migrated route.
2026-06-18 17:38:07 -07:00

101 lines
4.3 KiB
TypeScript

import { test, expect, type Page } from "@playwright/test";
import { MIGRATED_E2E_SEGMENTS } from "../../fixtures/migratedPages";
import { ADMIN_STORAGE_PATH } from "../../constants";
import { dismissFeedbackPopup } from "../../helpers/navigation";
/**
* App Router migration smoke as a user journey: start where the proxy lands you,
* click a migrated page in the sidebar, confirm it routed and rendered, reload it
* (the check a wrong server_root_path breaks), bounce to a legacy page and back,
* and, once two pages are migrated, navigate directly between two migrated pages.
*
* Driven by MIGRATED_E2E_SEGMENTS, so it grows as pages are migrated. Set
* SERVER_ROOT_PATH (e.g. "/litellm") to exercise the non-root mount; leave it
* unset for the default mount. Boot the proxy with the matching value first.
*/
const ROOT = process.env.SERVER_ROOT_PATH ?? "";
const esc = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const pathRe = (segment: string) => new RegExp(`${esc(ROOT)}/ui/${esc(segment)}/?($|\\?)`);
const virtualKeysLink = (page: Page) => page.getByRole("link", { name: "Virtual Keys", exact: true });
/** The dashboard shell is present (sidebar rendered); page didn't 404 / crash. */
async function expectRendered(page: Page) {
await expect(virtualKeysLink(page)).toBeVisible({ timeout: 20_000 });
}
/**
* Click a migrated page's sidebar link. Migrated items render as <a href=".../ui/<segment>">;
* nested ones live under collapsible submenus, so expand submenus until the link is clickable.
*/
async function clickSidebar(page: Page, segment: string) {
const link = page.locator(`a[href$="/ui/${segment}"]`).first();
for (let i = 0; i < 8 && !(await link.isVisible().catch(() => false)); i++) {
const collapsedSubmenu = page
.locator(".ant-menu-submenu:not(.ant-menu-submenu-open) > .ant-menu-submenu-title")
.first();
if (!(await collapsedSubmenu.isVisible().catch(() => false))) break;
await collapsedSubmenu.click();
await page.waitForTimeout(250);
}
await link.click();
}
test.use({ storageState: ADMIN_STORAGE_PATH });
test.describe("App Router migrated pages", () => {
for (const segment of MIGRATED_E2E_SEGMENTS) {
test(`${segment}: sidebar nav, reload, and round-trip via the api-keys landing`, async ({ page }) => {
const pageErrors: string[] = [];
page.on("pageerror", (e) => pageErrors.push(String(e)));
// 1. Start where the proxy lands us.
await page.goto(`${ROOT}/ui/`);
await dismissFeedbackPopup(page);
await expectRendered(page);
// 2. Click the migrated page in the sidebar -> path route + rendered.
await clickSidebar(page, segment);
await expect(page).toHaveURL(pathRe(segment));
await expectRendered(page);
// 3. Reload the path route directly; a wrong server_root_path 404s here.
await page.reload();
await dismissFeedbackPopup(page);
await expect(page).toHaveURL(pathRe(segment));
await expectRendered(page);
// 4. Click the Virtual Keys sidebar link to the api-keys landing (now a path route), then back.
await virtualKeysLink(page).click();
await expect(page).toHaveURL(pathRe("api-keys"));
await dismissFeedbackPopup(page);
await expectRendered(page);
// 5. Click back to the migrated page.
await clickSidebar(page, segment);
await expect(page).toHaveURL(pathRe(segment));
await expectRendered(page);
expect(pageErrors, `page errors during ${segment} journey`).toEqual([]);
});
}
test("navigates directly between two migrated pages", async ({ page }) => {
test.skip(MIGRATED_E2E_SEGMENTS.length < 2, "needs >= 2 migrated pages");
const [first, second] = MIGRATED_E2E_SEGMENTS;
const pageErrors: string[] = [];
page.on("pageerror", (e) => pageErrors.push(String(e)));
await page.goto(`${ROOT}/ui/`);
await dismissFeedbackPopup(page);
await clickSidebar(page, first);
await expect(page).toHaveURL(pathRe(first));
await expectRendered(page);
await clickSidebar(page, second);
await expect(page).toHaveURL(pathRe(second));
await expectRendered(page);
// Back to the first migrated page.
await clickSidebar(page, first);
await expect(page).toHaveURL(pathRe(first));
await expectRendered(page);
expect(pageErrors, "page errors during migrated -> migrated nav").toEqual([]);
});
});