From 643989989f46a26b4ad74b6fc1ca7b4fc4236f16 Mon Sep 17 00:00:00 2001 From: ryan-crabbe-berri Date: Fri, 22 May 2026 11:29:17 -0700 Subject: [PATCH] chore(test): remove dead old Playwright e2e suite (#28632) The Playwright suite under tests/proxy_admin_ui_tests/e2e_ui_tests/ is no longer wired into CI (only test_*.py is globbed) and every active spec is duplicated by ui/litellm-dashboard/e2e_tests/tests/ (login, auth redirect, search users, internal user list). team_admin.spec.ts was entirely commented out. Removing the directory plus its only-used-here playwright config, package.json/lock, and utils/login.ts keeps the canonical suite under ui/litellm-dashboard/e2e_tests/ as the single source of truth. --- .../e2e_ui_tests/login_to_ui.spec.ts | 51 ---- .../e2e_ui_tests/redirect-fail-screenshot.png | Bin 49131 -> 0 bytes .../require_auth_for_dashboard.spec.ts | 37 --- .../e2e_ui_tests/search_users.spec.ts | 222 ---------------- .../e2e_ui_tests/team_admin.spec.ts | 250 ------------------ .../e2e_ui_tests/view_internal_user.spec.ts | 72 ----- .../e2e_ui_tests/view_user_info.spec.ts | 124 --------- tests/proxy_admin_ui_tests/package-lock.json | 97 ------- tests/proxy_admin_ui_tests/package.json | 14 - .../proxy_admin_ui_tests/playwright.config.ts | 84 ------ tests/proxy_admin_ui_tests/utils/login.ts | 27 -- 11 files changed, 978 deletions(-) delete mode 100644 tests/proxy_admin_ui_tests/e2e_ui_tests/login_to_ui.spec.ts delete mode 100644 tests/proxy_admin_ui_tests/e2e_ui_tests/redirect-fail-screenshot.png delete mode 100644 tests/proxy_admin_ui_tests/e2e_ui_tests/require_auth_for_dashboard.spec.ts delete mode 100644 tests/proxy_admin_ui_tests/e2e_ui_tests/search_users.spec.ts delete mode 100644 tests/proxy_admin_ui_tests/e2e_ui_tests/team_admin.spec.ts delete mode 100644 tests/proxy_admin_ui_tests/e2e_ui_tests/view_internal_user.spec.ts delete mode 100644 tests/proxy_admin_ui_tests/e2e_ui_tests/view_user_info.spec.ts delete mode 100644 tests/proxy_admin_ui_tests/package-lock.json delete mode 100644 tests/proxy_admin_ui_tests/package.json delete mode 100644 tests/proxy_admin_ui_tests/playwright.config.ts delete mode 100644 tests/proxy_admin_ui_tests/utils/login.ts diff --git a/tests/proxy_admin_ui_tests/e2e_ui_tests/login_to_ui.spec.ts b/tests/proxy_admin_ui_tests/e2e_ui_tests/login_to_ui.spec.ts deleted file mode 100644 index e5a397a6a6..0000000000 --- a/tests/proxy_admin_ui_tests/e2e_ui_tests/login_to_ui.spec.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* - -Login to Admin UI -Basic UI Test - -Click on all the tabs ensure nothing is broken -*/ - -import { test, expect } from "@playwright/test"; - -test("admin login test", async ({ page }) => { - // Go to the specified URL - await page.goto("http://localhost:4000/ui"); - await page.waitForLoadState("networkidle"); - - await page.screenshot({ path: "test-results/login_before.png" }); - - // Enter "admin" in the username input field - await page.fill('input[placeholder="Enter your username"]', "admin"); - - // Enter "gm" in the password input field - await page.fill('input[placeholder="Enter your password"]', "gm"); - - page.screenshot({ path: "test-results/login_after_inputs.png" }); - - // Optionally, you can add an assertion to verify the login button is enabled - const loginButton = page.getByRole("button", { name: "Login" }); - await expect(loginButton).toBeEnabled(); - - // Optionally, you can click the login button to submit the form - await loginButton.click(); - const tabs = [ - "Virtual Keys", - "Playground", - "Models", - "Usage", - "Teams", - "Internal User", - "Settings", - "Experimental", - "API Reference", - "AI Hub", - ]; - - for (const tab of tabs) { - const tabElement = page.locator("span.ant-menu-title-content", { - hasText: tab, - }); - await tabElement.click(); - } -}); diff --git a/tests/proxy_admin_ui_tests/e2e_ui_tests/redirect-fail-screenshot.png b/tests/proxy_admin_ui_tests/e2e_ui_tests/redirect-fail-screenshot.png deleted file mode 100644 index b2e332512608703b6acb1c7839e7f40fc7460ab8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 49131 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 Wc$`4lAeHy?F6tPZE6}q4{a*k&kMIfr diff --git a/tests/proxy_admin_ui_tests/e2e_ui_tests/require_auth_for_dashboard.spec.ts b/tests/proxy_admin_ui_tests/e2e_ui_tests/require_auth_for_dashboard.spec.ts deleted file mode 100644 index 4e4bd2fcd9..0000000000 --- a/tests/proxy_admin_ui_tests/e2e_ui_tests/require_auth_for_dashboard.spec.ts +++ /dev/null @@ -1,37 +0,0 @@ -// tests/auth.spec.ts -import { test, expect } from "@playwright/test"; - -test.describe("Authentication Checks", () => { - test("should redirect unauthenticated user from a protected page", async ({ - page, - }) => { - test.setTimeout(30000); - - page.on("console", (msg) => console.log("PAGE LOG:", msg.text())); - - const protectedPageUrl = "http://localhost:4000/ui?page=llm-playground"; - const expectedRedirectUrl = "http://localhost:4000/ui/login/"; - - console.log( - `Attempting to navigate to protected page: ${protectedPageUrl}` - ); - - await page.goto(protectedPageUrl); - - console.log(`Navigation initiated. Current URL: ${page.url()}`); - - try { - await page.waitForURL(expectedRedirectUrl, { timeout: 10000 }); - console.log(`Waited for URL. Current URL is now: ${page.url()}`); - } catch (error) { - console.error( - `Timeout waiting for URL: ${expectedRedirectUrl}. Current URL: ${page.url()}` - ); - await page.screenshot({ path: "redirect-fail-screenshot.png" }); - throw error; - } - - await expect(page).toHaveURL(expectedRedirectUrl); - console.log(`Assertion passed: Page URL is ${expectedRedirectUrl}`); - }); -}); diff --git a/tests/proxy_admin_ui_tests/e2e_ui_tests/search_users.spec.ts b/tests/proxy_admin_ui_tests/e2e_ui_tests/search_users.spec.ts deleted file mode 100644 index d72c44ab8c..0000000000 --- a/tests/proxy_admin_ui_tests/e2e_ui_tests/search_users.spec.ts +++ /dev/null @@ -1,222 +0,0 @@ -/* -Search Users in Admin UI -E2E Test for user search functionality - -Tests: -1. Navigate to Internal Users tab -2. Verify search input exists -3. Test search functionality -4. Verify results update -5. Test filtering by email, user ID, and SSO user ID -*/ - -import { test, expect } from "@playwright/test"; - -test("user search test", async ({ page }) => { - // Set a longer timeout for the entire test - test.setTimeout(60000); - - // Enable console logging - page.on("console", (msg) => console.log("PAGE LOG:", msg.text())); - - // Login first - await page.goto("http://localhost:4000/ui"); - await page.waitForLoadState("networkidle"); - console.log("Navigated to login page"); - - page.screenshot({ path: "test-results/search_users_before_login.png" }); - - // Wait for login form to be visible - await page.waitForSelector('input[placeholder="Enter your username"]', { - timeout: 10000, - }); - console.log("Login form is visible"); - - await page.fill('input[placeholder="Enter your username"]', "admin"); - await page.fill('input[placeholder="Enter your password"]', "gm"); - console.log("Filled login credentials"); - - const loginButton = page.getByRole("button", { name: "Login" }); - await expect(loginButton).toBeEnabled(); - await loginButton.click(); - console.log("Clicked login button"); - - // Wait for navigation to complete and dashboard to load - await page.waitForLoadState("networkidle"); - console.log("Page loaded after login"); - - // Take a screenshot for debugging - await page.screenshot({ path: "after-login.png" }); - console.log("Took screenshot after login"); - - // Try to find the Internal User tab with more debugging - console.log("Looking for Internal User tab..."); - const internalUserTab = page.locator("span.ant-menu-title-content", { - hasText: "Internal User", - }); - - // Wait for the tab to be visible - await internalUserTab.waitFor({ state: "visible", timeout: 10000 }); - console.log("Internal User tab is visible"); - - // Take another screenshot before clicking - await page.screenshot({ path: "before-tab-click.png" }); - console.log("Took screenshot before tab click"); - - await internalUserTab.click(); - console.log("Clicked Internal User tab"); - - // Wait for the page to load and table to be visible - await page.waitForSelector("tbody tr", { timeout: 30000 }); - await page.waitForTimeout(2000); // Additional wait for table to stabilize - console.log("Table is visible"); - - // Take a final screenshot - await page.screenshot({ path: "after-tab-click.png" }); - console.log("Took screenshot after tab click"); - - // Verify search input exists - const searchInput = page.locator('input[placeholder="Search by email..."]'); - await expect(searchInput).toBeVisible(); - console.log("Search input is visible"); - - // Test search functionality - const initialUserCount = await page.locator("tbody tr").count(); - console.log(`Initial user count: ${initialUserCount}`); - - // Perform a search - const testEmail = "test@"; - await searchInput.fill(testEmail); - console.log("Filled search input"); - - // Wait for the debounced search to complete - await page.waitForTimeout(500); - console.log("Waited for debounce"); - - // Wait for the results count to update - await page.waitForFunction((initialCount) => { - const currentCount = document.querySelectorAll("tbody tr").length; - return currentCount !== initialCount; - }, initialUserCount); - console.log("Results updated"); - - const filteredUserCount = await page.locator("tbody tr").count(); - console.log(`Filtered user count: ${filteredUserCount}`); - - expect(filteredUserCount).toBeDefined(); - - // Clear the search - await searchInput.clear(); - console.log("Cleared search"); - - await page.waitForTimeout(500); - console.log("Waited for debounce after clear"); - - await page.waitForFunction((initialCount) => { - const currentCount = document.querySelectorAll("tbody tr").length; - return currentCount === initialCount; - }, initialUserCount); - console.log("Results reset"); - - const resetUserCount = await page.locator("tbody tr").count(); - console.log(`Reset user count: ${resetUserCount}`); - - expect(resetUserCount).toBe(initialUserCount); -}); - -test("user filter test", async ({ page }) => { - // Set a longer timeout for the entire test - test.setTimeout(60000); - - // Enable console logging - page.on("console", (msg) => console.log("PAGE LOG:", msg.text())); - - // Login first - await page.goto("http://localhost:4000/ui"); - await page.waitForLoadState("networkidle"); - console.log("Navigated to login page"); - - // Wait for login form to be visible - await page.waitForSelector('input[placeholder="Enter your username"]', { - timeout: 10000, - }); - console.log("Login form is visible"); - - await page.fill('input[placeholder="Enter your username"]', "admin"); - await page.fill('input[placeholder="Enter your password"]', "gm"); - console.log("Filled login credentials"); - - const loginButton = page.getByRole("button", { name: "Login" }); - await expect(loginButton).toBeEnabled(); - await loginButton.click(); - console.log("Clicked login button"); - - // Wait for navigation to complete and dashboard to load - await page.waitForLoadState("networkidle"); - console.log("Page loaded after login"); - - // Navigate to Internal Users tab - const internalUserTab = page.locator("span.ant-menu-title-content", { - hasText: "Internal User", - }); - await internalUserTab.waitFor({ state: "visible", timeout: 10000 }); - await internalUserTab.click(); - console.log("Clicked Internal User tab"); - - // Wait for the page to load and table to be visible - await page.waitForSelector("tbody tr", { timeout: 30000 }); - await page.waitForTimeout(2000); // Additional wait for table to stabilize - console.log("Table is visible"); - - // Get initial user count - const initialUserCount = await page.locator("tbody tr").count(); - console.log(`Initial user count: ${initialUserCount}`); - - // Click the filter button to show additional filters - const filterButton = page.getByRole("button", { - name: "Filters", - exact: true, - }); - await filterButton.click(); - console.log("Clicked filter button"); - await page.waitForTimeout(500); // Wait for filters to appear - - // Test user ID filter - const userIdInput = page.locator('input[placeholder="Filter by User ID"]'); - await expect(userIdInput).toBeVisible(); - console.log("User ID filter is visible"); - - await userIdInput.fill("user"); - console.log("Filled user ID filter"); - await page.waitForTimeout(1000); - const userIdFilteredCount = await page.locator("tbody tr").count(); - console.log(`User ID filtered count: ${userIdFilteredCount}`); - expect(userIdFilteredCount).toBeLessThan(initialUserCount); - - // Clear user ID filter - await userIdInput.clear(); - await page.waitForTimeout(1000); - console.log("Cleared user ID filter"); - - // Test SSO user ID filter - const ssoUserIdInput = page.locator('input[placeholder="Filter by SSO ID"]'); - await expect(ssoUserIdInput).toBeVisible(); - console.log("SSO user ID filter is visible"); - - await ssoUserIdInput.fill("sso"); - console.log("Filled SSO user ID filter"); - await page.waitForTimeout(1000); - const ssoUserIdFilteredCount = await page.locator("tbody tr").count(); - console.log(`SSO user ID filtered count: ${ssoUserIdFilteredCount}`); - expect(ssoUserIdFilteredCount).toBeLessThan(initialUserCount); - - // Clear SSO user ID filter - await ssoUserIdInput.clear(); - await page.waitForTimeout(5000); - console.log("Cleared SSO user ID filter"); - - // Verify count returns to initial after clearing all filters - const finalUserCount = await page.locator("tbody tr").count(); - console.log(`Final user count: ${finalUserCount}`); - expect(finalUserCount).toBe(initialUserCount); -}); diff --git a/tests/proxy_admin_ui_tests/e2e_ui_tests/team_admin.spec.ts b/tests/proxy_admin_ui_tests/e2e_ui_tests/team_admin.spec.ts deleted file mode 100644 index a753c724b3..0000000000 --- a/tests/proxy_admin_ui_tests/e2e_ui_tests/team_admin.spec.ts +++ /dev/null @@ -1,250 +0,0 @@ -import { test, expect } from "@playwright/test"; -import { loginToUI } from "../utils/login"; - -// test.describe("Invite User, Set Password, and Login", () => { -// let testEmail: string; -// const testPassword = "Password123!"; // Define a password -// const teamName1 = `team-invite-test-1-${Date.now()}`; -// const teamName2 = `team-invite-test-2-${Date.now()}`; -// const keyName1 = `key-${teamName1}`; -// const keyName2 = `key-${teamName2}`; - -// test.beforeEach(async ({ page }) => { -// await loginToUI(page); // Login as admin first -// await page.goto("http://localhost:4000/ui?page=teams"); - -// // --- Create Team 1 --- -// await page.getByRole("button", { name: "+ Create New Team" }).click(); -// await page -// .getByLabel("Team Name") -// .waitFor({ state: "visible", timeout: 5000 }); // Wait for label -// await page.getByLabel("Team Name").click(); -// await page.getByLabel("Team Name").fill(teamName1); -// await page.getByRole("button", { name: "Create Team" }).click(); -// // Wait for the modal to close or for a success message if applicable -// await expect( -// page.locator(".ant-modal-wrap").filter({ hasText: "Create New Team" }) -// ).not.toBeVisible({ timeout: 10000 }); -// console.log(`Created Team 1: ${teamName1}`); - -// // --- Create Team 2 --- -// await page.getByRole("button", { name: "+ Create New Team" }).click(); -// await page -// .getByLabel("Team Name") -// .waitFor({ state: "visible", timeout: 5000 }); // Wait for label -// await page.getByLabel("Team Name").click(); -// await page.getByLabel("Team Name").fill(teamName2); -// await page.getByRole("button", { name: "Create Team" }).click(); -// // Wait for the modal to close or for a success message if applicable -// await expect( -// page.locator(".ant-modal-wrap").filter({ hasText: "Create New Team" }) -// ).not.toBeVisible({ timeout: 10000 }); -// console.log(`Created Team 2: ${teamName2}`); - -// // // Verify both teams are listed -// // await page.goto("http://localhost:4000/ui?page=teams"); // Refresh or ensure on teams page -// // await page.waitForTimeout(3000); -// await expect(page.getByText(teamName1)).toBeVisible({ timeout: 10000 }); -// await expect(page.getByText(teamName2)).toBeVisible({ timeout: 10000 }); - -// // --- Navigate to Keys Page --- -// await page.goto("http://localhost:4000/ui?page=api-keys"); -// await page.waitForTimeout(3000); -// await expect( -// page.getByRole("button", { name: "+ Create New Key" }) -// ).toBeVisible(); // Wait for page load - -// // --- Create Key for Team 1 --- -// await page.getByRole("button", { name: "+ Create New Key" }).click(); -// const createKeyModal1 = page -// .locator(".ant-modal-wrap") -// .filter({ hasText: "Key Ownership" }); -// await expect(createKeyModal1).toBeVisible(); - -// // Select Team 1 -// await createKeyModal1 -// .locator(".ant-select-selector >> input") -// .first() -// .click(); // Click to open team dropdown -// await createKeyModal1 -// .locator(".ant-select-selector >> input") -// .first() -// .fill(teamName1); - -// await page -// .locator(".ant-select-item-option") -// .filter({ hasText: teamName1 }) -// .first() -// .click(); // Click specific team name - -// // Enter Key Name 1 -// await page.fill('input[id="key_alias"]', keyName1); - -// // Click on models dropdown -// await page.locator("input#models").click(); -// await page.waitForSelector( -// '.ant-select-item-option[title="All Team Models"]' -// ); -// await page -// .locator('.ant-select-item-option[title="All Team Models"]') -// .click(); - -// // Click Create Key -// await createKeyModal1.getByRole("button", { name: "Create Key" }).click(); - -// // Close the Key Generated modal (which appears after successful creation) -// const keyGeneratedModal1 = page -// .locator(".ant-modal-wrap") -// .filter({ hasText: "Save your Key" }); -// await expect(keyGeneratedModal1).toBeVisible({ timeout: 10000 }); -// await keyGeneratedModal1.locator('button[aria-label="Close"]').click(); -// await expect(keyGeneratedModal1).not.toBeVisible(); // Wait for close -// console.log(`Created Key 1: ${keyName1} for Team: ${teamName1}`); - -// // --- Create Key for Team 2 --- -// await page.getByRole("button", { name: "+ Create New Key" }).click(); -// const createKeyModal2 = page -// .locator(".ant-modal-wrap") -// .filter({ hasText: "Key Ownership" }); -// await expect(createKeyModal2).toBeVisible(); - -// // Select Team 2 -// await createKeyModal2 -// .locator(".ant-select-selector >> input") -// .first() -// .click(); // Click to open team dropdown -// await page -// .locator(".ant-select-item-option") -// .filter({ hasText: teamName2 }) -// .click(); // Click specific team name - -// // Enter Key Name 2 -// await page.fill('input[id="key_alias"]', keyName2); - -// // Click on models dropdown -// await page.locator("input#models").click(); -// await page.waitForSelector( -// '.ant-select-item-option[title="All Team Models"]' -// ); -// await page -// .locator('.ant-select-item-option[title="All Team Models"]') -// .click(); - -// // Click Create Key -// await createKeyModal2.getByRole("button", { name: "Create Key" }).click(); - -// // Close the Key Generated modal -// const keyGeneratedModal2 = page -// .locator(".ant-modal-wrap") -// .filter({ hasText: "Save your Key" }); -// await expect(keyGeneratedModal2).toBeVisible({ timeout: 10000 }); -// await keyGeneratedModal2.locator('button[aria-label="Close"]').click(); -// await expect(keyGeneratedModal2).not.toBeVisible(); // Wait for close -// console.log(`Created Key 2: ${keyName2} for Team: ${teamName2}`); -// }); - -// test("Invite user, set password via link, and login", async ({ page }) => { -// // Navigate to Users page -// await page.goto("http://localhost:4000/ui?page=users"); - -// // Go to Internal User tab -// const internalUserTab = page.locator("span.ant-menu-title-content", { -// hasText: "Internal User", -// }); -// await internalUserTab.waitFor({ state: "visible", timeout: 10000 }); -// await internalUserTab.click(); - -// // --- Invite User Flow --- -// await page.getByRole("button", { name: "+ Invite User" }).click(); - -// // Wait for the invite user modal to be visible -// const inviteModal = page -// .locator(".ant-modal-wrap") -// .filter({ hasText: "Invite User" }); -// await expect(inviteModal).toBeVisible(); - -// testEmail = `test-${Date.now()}@litellm.ai`; // Use a unique email -// // Assuming the email input is the first one with 'base-input' test id inside the modal -// await inviteModal.getByTestId("base-input").first().fill(testEmail); - -// // Select Global Admin Role (or another appropriate role) -// const globalRoleLabel = inviteModal.getByLabel("Global Proxy Role"); -// await globalRoleLabel.click(); -// // Wait for the dropdown option to be visible before clicking -// const adminRoleOption = page.getByTitle("Admin (All Permissions)", { -// exact: true, -// }); -// await adminRoleOption.waitFor({ state: "visible", timeout: 5000 }); -// await adminRoleOption.click(); - -// // Select Team - Add explicit wait before clicking -// const teamIdLabel = inviteModal.getByLabel("Team ID"); -// // Wait for the label associated with the Team ID select to be visible -// await teamIdLabel.waitFor({ state: "visible", timeout: 10000 }); // Increased timeout for safety -// await teamIdLabel.click(); - -// // Wait for the team name option to be visible in the dropdown -// const teamNameOption = page.getByText(teamName1, { exact: true }); -// await teamNameOption.waitFor({ state: "visible", timeout: 5000 }); -// await teamNameOption.click(); - -// // Create User -// await inviteModal.getByRole("button", { name: "Create User" }).click(); - -// // --- Capture Invitation Link --- -// const invitationModal = page -// .locator(".ant-modal-wrap") -// .filter({ hasText: "Invitation Link" }); -// await expect(invitationModal).toBeVisible({ timeout: 15000 }); // Wait longer for modal - -// // Locate the text element containing the URL more reliably -// const invitationUrl = await page -// .locator("div.flex.justify-between.pt-5.pb-2") // find the correct div -// .filter({ hasText: "Invitation Link" }) // find the div that has text "Invitation Link" -// .locator("p") // find all

inside that div -// .nth(1) // pick the second

(index 1) -// .innerText(); - -// // Close Invitation Link Modal -// await page -// .locator(".ant-modal-wrap") -// .filter({ hasText: "Invitation Link" }) -// .locator('button[aria-label="Close"]') -// .click(); - -// // Close Invite User Modal -// await page -// .locator(".ant-modal-wrap") -// .filter({ hasText: "Invite User" }) -// .locator('button[aria-label="Close"]') -// .click(); - -// // Open invite link as new page (simulate invited user) -// const context = await page.context()?.browser()?.newContext(); -// const invitedUserPage = await context?.newPage(); -// if (!invitedUserPage) { -// throw new Error("invitedUserPage is undefined"); -// } -// await invitedUserPage?.goto(invitationUrl || ""); - -// //Insert new password -// await invitedUserPage?.fill("input#password", testPassword); - -// //Click on submit -// await invitedUserPage?.getByRole("button", { name: "Sign Up" }).click(); - -// // // --- Verify Keys Created --- -// // await invitedUserPage?.waitForSelector("table"); - -// // // Verify keyName1 (associated with user's team) IS visible in the table -// // const keyTable = invitedUserPage.locator('table'); // Locate the table element -// // await expect(keyTable).toBeVisible({ timeout: 10000 }); // Ensure table exists -// // // Use getByText within the table scope to find the key name -// // await expect(keyTable.getByText(keyName1, { exact: true })).toBeVisible({ timeout: 10000 }); -// // console.log(`Verified key ${keyName1} is visible for user ${testEmail}`); - -// // // Verify keyName2 (associated with the *other* team) IS NOT visible -// // await expect(keyTable.getByText(keyName2, { exact: true })).not.toBeVisible(); -// // console.log(`Verified key ${keyName2} is NOT visible for user ${testEmail}`); -// }); -// }); diff --git a/tests/proxy_admin_ui_tests/e2e_ui_tests/view_internal_user.spec.ts b/tests/proxy_admin_ui_tests/e2e_ui_tests/view_internal_user.spec.ts deleted file mode 100644 index 832832d8ae..0000000000 --- a/tests/proxy_admin_ui_tests/e2e_ui_tests/view_internal_user.spec.ts +++ /dev/null @@ -1,72 +0,0 @@ -/* -Test view internal user page -*/ - -import { test, expect } from "@playwright/test"; - -test("view internal user page", async ({ page }) => { - // Go to the specified URL - await page.goto("http://localhost:4000/ui"); - await page.waitForLoadState("networkidle"); - - page.screenshot({ path: "test-results/view_internal_user_before_login.png" }); - - // Enter "admin" in the username input field - await page.fill('input[placeholder="Enter your username"]', "admin"); - - // Enter "gm" in the password input field - await page.fill('input[placeholder="Enter your password"]', "gm"); - - // Click the login button - const loginButton = page.getByRole("button", { name: "Login" }); - await expect(loginButton).toBeEnabled(); - await loginButton.click(); - - // Wait for the Internal User tab and click it - const tabElement = page.locator("span.ant-menu-title-content", { - hasText: "Internal User", - }); - await tabElement.click(); - - // Wait for the table to load - await page.waitForSelector("tbody tr", { timeout: 10000 }); - await page.waitForTimeout(2000); // Additional wait for table to stabilize - await page.waitForLoadState("networkidle"); - - // Test all expected fields are present - // Verify that the API Keys column is rendered for all users - // The UI renders badges in each row - we just verify the column structure exists - const rowCount = await page.locator("tbody tr").count(); - expect(rowCount).toBeGreaterThan(0); - - const userIdHeader = await page.locator("th", { hasText: "User ID" }); - await expect(userIdHeader).toBeVisible({ timeout: 10000 }); - - // test pagination - // Wait for pagination controls to be visible - await page.waitForSelector(".flex.justify-between.items-center", { - timeout: 5000, - }); - - // Check if we're on the first page by looking at the results count - const resultsText = - (await page.locator(".text-sm.text-gray-700").textContent()) || ""; - const isFirstPage = resultsText.includes("1 -"); - - if (isFirstPage) { - // On first page, previous button should be disabled - const prevButton = page.locator("button", { hasText: "Previous" }); - await expect(prevButton).toBeDisabled(); - } - - // Next button should be enabled if there are more pages - const nextButton = page.locator("button", { hasText: "Next" }); - const totalResults = - (await page.locator(".text-sm.text-gray-700").textContent()) || ""; - const hasMorePages = - totalResults.includes("of") && !totalResults.includes("1 - 25 of 25"); - - if (hasMorePages) { - await expect(nextButton).toBeEnabled(); - } -}); diff --git a/tests/proxy_admin_ui_tests/e2e_ui_tests/view_user_info.spec.ts b/tests/proxy_admin_ui_tests/e2e_ui_tests/view_user_info.spec.ts deleted file mode 100644 index adda3088f1..0000000000 --- a/tests/proxy_admin_ui_tests/e2e_ui_tests/view_user_info.spec.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { test, expect } from "@playwright/test"; -import { loginToUI } from "../utils/login"; - -test.describe("User Info View", () => { - test("should display user info when clicking on user ID", async ({ - page, - }) => { - await page.goto("http://localhost:4000/ui"); - await page.waitForLoadState("networkidle"); - - page.screenshot({ - path: "test-results/view_user_info_before_login.png", - }); - - // Enter "admin" in the username input field - await page.fill('input[placeholder="Enter your username"]', "admin"); - page.screenshot({ - path: "test-results/view_user_info_after_username_input.png", - }); - - // Enter "gm" in the password input field - await page.fill('input[placeholder="Enter your password"]', "gm"); - page.screenshot({ - path: "test-results/view_user_info_after_password_input.png", - }); - - // Click the login button - const loginButton = page.getByRole("button", { name: "Login" }); - await expect(loginButton).toBeEnabled(); - await loginButton.click(); - page.screenshot({ - path: "test-results/view_user_info_after_login_button_click.png", - }); - - // Wait for navigation to complete and dashboard to load - await page.waitForLoadState("networkidle"); - const tabElement = page.locator("span.ant-menu-title-content", { - hasText: "Internal User", - }); - await tabElement.click(); - page.screenshot({ - path: "test-results/view_user_info_after_internal_user_tab_click.png", - }); - // Wait for loading state to disappear - await page.waitForSelector('text="🚅 Loading users..."', { - state: "hidden", - timeout: 10000, - }); - page.screenshot({ path: "test-results/view_user_info_after_loading.png" }); - // Wait for users table to load - await page.waitForSelector("table"); - page.screenshot({ - path: "test-results/view_user_info_after_table_load.png", - }); - // Get the first user ID cell - const firstUserIdCell = page.locator( - "table tbody tr:first-child td:first-child" - ); - const userId = await firstUserIdCell.textContent(); - console.log("Found user ID:", userId); - - // Click on the user ID - await firstUserIdCell.click(); - await page.waitForLoadState("networkidle"); - - // Check for tabs - await expect(page.locator('button:has-text("Overview")')).toBeVisible({ - timeout: 10000, - }); - await expect(page.locator('button:has-text("Details")')).toBeVisible({ - timeout: 10000, - }); - - // Switch to details tab - await page.locator('button:has-text("Details")').click(); - - // Check details section - await expect(page.locator("text=User ID")).toBeVisible(); - await expect(page.locator("text=Email")).toBeVisible(); - - // Go back to users list - await page.locator('button:has-text("Back to Users")').click(); - - // Verify we're back on the users page - await expect(page.locator("table")).toBeVisible(); - await expect( - page.locator('input[placeholder="Search by email..."]') - ).toBeVisible(); - }); - - // test("should handle user deletion", async ({ page }) => { - // // Wait for users table to load - // await page.waitForSelector("table"); - - // // Get the first user ID cell - // const firstUserIdCell = page.locator( - // "table tbody tr:first-child td:first-child" - // ); - // const userId = await firstUserIdCell.textContent(); - - // // Click on the user ID - // await firstUserIdCell.click(); - - // // Wait for user info view to load - // await page.waitForSelector('h1:has-text("User")'); - - // // Click delete button - // await page.locator('button:has-text("Delete User")').click(); - - // // Confirm deletion in modal - // await page.locator('button:has-text("Delete")').click(); - - // // Verify success message - // await expect(page.locator("text=User deleted successfully")).toBeVisible(); - - // // Verify we're back on the users page - // await expect(page.locator('h1:has-text("Users")')).toBeVisible(); - - // // Verify user is no longer in the table - // if (userId) { - // await expect(page.locator(`text=${userId}`)).not.toBeVisible(); - // } - // }); -}); diff --git a/tests/proxy_admin_ui_tests/package-lock.json b/tests/proxy_admin_ui_tests/package-lock.json deleted file mode 100644 index 8c79edf9ad..0000000000 --- a/tests/proxy_admin_ui_tests/package-lock.json +++ /dev/null @@ -1,97 +0,0 @@ -{ - "name": "proxy_admin_ui_tests", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "proxy_admin_ui_tests", - "version": "1.0.0", - "license": "ISC", - "devDependencies": { - "@playwright/test": "^1.47.2", - "@types/node": "^22.5.5" - } - }, - "node_modules/@playwright/test": { - "version": "1.56.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz", - "integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "playwright": "1.56.1" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@types/node": { - "version": "22.19.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.1.tgz", - "integrity": "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/playwright": { - "version": "1.56.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz", - "integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "playwright-core": "1.56.1" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "fsevents": "2.3.2" - } - }, - "node_modules/playwright-core": { - "version": "1.56.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz", - "integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "playwright-core": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, - "license": "MIT" - } - } -} diff --git a/tests/proxy_admin_ui_tests/package.json b/tests/proxy_admin_ui_tests/package.json deleted file mode 100644 index 5933490fb1..0000000000 --- a/tests/proxy_admin_ui_tests/package.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "proxy_admin_ui_tests", - "version": "1.0.0", - "description": "", - "main": "index.js", - "scripts": {}, - "keywords": [], - "author": "", - "license": "ISC", - "devDependencies": { - "@playwright/test": "1.56.1", - "@types/node": "22.19.1" - } -} diff --git a/tests/proxy_admin_ui_tests/playwright.config.ts b/tests/proxy_admin_ui_tests/playwright.config.ts deleted file mode 100644 index 8b66c47394..0000000000 --- a/tests/proxy_admin_ui_tests/playwright.config.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { defineConfig, devices } from '@playwright/test'; - -/** - * Read environment variables from file. - * https://github.com/motdotla/dotenv - */ -// import dotenv from 'dotenv'; -// import path from 'path'; -// dotenv.config({ path: path.resolve(__dirname, '.env') }); - -/** - * See https://playwright.dev/docs/test-configuration. - */ -export default defineConfig({ - testDir: './e2e_ui_tests', - testIgnore: ['**/tests/pass_through_tests/**', '../pass_through_tests/**/*'], - testMatch: '**/*.spec.ts', // Only run files ending in .spec.ts - /* Run tests in files in parallel */ - fullyParallel: true, - /* Fail the build on CI if you accidentally left test.only in the source code. */ - forbidOnly: !!process.env.CI, - /* Retry on CI only */ - retries: process.env.CI ? 2 : 0, - /* Opt out of parallel tests on CI. */ - workers: process.env.CI ? 1 : undefined, - /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: 'html', - /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ - use: { - /* Base URL to use in actions like `await page.goto('/')`. */ - // baseURL: 'http://127.0.0.1:3000', - - /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ - trace: 'on-first-retry', - }, - - /* Configure projects for major browsers */ - projects: [ - { - name: 'chromium', - use: { ...devices['Desktop Chrome'] }, - }, - - { - name: 'firefox', - use: { ...devices['Desktop Firefox'] }, - }, - - { - name: 'webkit', - use: { ...devices['Desktop Safari'] }, - }, - - /* Test against mobile viewports. */ - // { - // name: 'Mobile Chrome', - // use: { ...devices['Pixel 5'] }, - // }, - // { - // name: 'Mobile Safari', - // use: { ...devices['iPhone 12'] }, - // }, - - /* Test against branded browsers. */ - // { - // name: 'Microsoft Edge', - // use: { ...devices['Desktop Edge'], channel: 'msedge' }, - // }, - // { - // name: 'Google Chrome', - // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, - // }, - ], - timeout: 4*60*1000, - expect: { - timeout: 10 * 1000 - } - /* Run your local dev server before starting the tests */ - // webServer: { - // command: 'npm run start', - // url: 'http://127.0.0.1:3000', - // reuseExistingServer: !process.env.CI, - // }, -}); diff --git a/tests/proxy_admin_ui_tests/utils/login.ts b/tests/proxy_admin_ui_tests/utils/login.ts deleted file mode 100644 index 25858d9f57..0000000000 --- a/tests/proxy_admin_ui_tests/utils/login.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Page, expect } from "@playwright/test"; - -export async function loginToUI(page: Page) { - // Login first - await page.goto("http://localhost:4000/ui"); - await page.waitForLoadState("networkidle"); - console.log("Navigated to login page"); - - page.screenshot({ path: "test-results/login_utils_before.png" }); - // Wait for login form to be visible - await page.waitForSelector('input[placeholder="Enter your username"]', { - timeout: 10000, - }); - console.log("Login form is visible"); - - await page.fill('input[placeholder="Enter your username"]', "admin"); - await page.fill('input[placeholder="Enter your password"]', "gm"); - console.log("Filled login credentials"); - - const loginButton = page.getByRole("button", { name: "Login" }); - await expect(loginButton).toBeEnabled(); - await loginButton.click(); - console.log("Clicked login button"); - - // Wait for navigation to complete - await page.waitForURL("**/*"); -}