import { test, expect } from "@playwright/test"; import { TEST_USERS } from "../constants/test-users"; // Use admin auth state for all tests in this file test.use({ storageState: TEST_USERS.admin.authFile }); test.describe("Admin User Detail Page", () => { test.describe("Basic Functionality", () => { test.beforeEach(async ({ page }) => { // Navigate to users list first await page.goto("/admin/users"); await page.waitForLoadState("networkidle"); // Wait for the table to load - using a more generic selector await page.waitForSelector("table", { timeout: 10000 }); // Get the second row (first non-header row with actual user) const userRows = page.locator("table tbody tr"); const rowCount = await userRows.count(); if (rowCount > 0) { // Click on the first user row to navigate to detail await userRows.first().click(); await page.waitForURL(/\/admin\/users\/.+/); await page.waitForLoadState("networkidle"); } }); }); test.describe("User Information Display", () => { test("should display all user information fields correctly", async ({ page, }) => { await page.goto("/admin/users"); await page.waitForSelector("[data-testid='users-table']"); const userRow = page .locator("[data-testid='users-table'] tbody tr") .first(); await userRow.click(); await page.waitForURL(/\/admin\/users\/.+/); // Check form fields are editable await expect(page.getByTestId("user-name-input")).toBeVisible(); await expect(page.getByTestId("user-email-input")).toBeVisible(); await expect(page.getByTestId("save-changes-button")).toBeVisible(); }); test("should display proper status badge based on user state", async ({ page, }) => { await page.goto("/admin/users"); await page.waitForSelector("[data-testid='users-table']"); const userRow = page .locator("[data-testid='users-table'] tbody tr") .first(); await userRow.click(); await page.waitForURL(/\/admin\/users\/.+/); await page.waitForLoadState("networkidle"); // Should have either active or banned status badge const activeBadge = page.getByTestId("status-badge-active"); const bannedBadge = page.getByTestId("status-badge-banned"); const hasStatusBadge = (await activeBadge.isVisible({ timeout: 1000 }).catch(() => false)) || (await bannedBadge.isVisible({ timeout: 1000 }).catch(() => false)); expect(hasStatusBadge).toBe(true); }); }); test.describe("Statistics", () => { test("should display stats for user with AI activity", async ({ page }) => { await page.goto("/admin/users"); await page.waitForSelector("[data-testid='users-table']"); // Search for admin user (likely to have activity) const searchInput = page.getByTestId("users-search-input"); await searchInput.fill("Admin"); await searchInput.press("Enter"); await page.waitForTimeout(500); // Navigate to user detail const userRow = page .locator("[data-testid='users-table'] tbody tr") .first(); await userRow.click(); await page.waitForURL(/\/admin\/users\/.+/); // Click on statistics tab // Wait for stats to load await page.waitForSelector("[data-testid='user-statistics-card']", { timeout: 2000, }); // Check for activity indicators (stats grid should be visible) const statsGrid = page.locator("[data-testid='stats-grid']"); if (await statsGrid.isVisible({ timeout: 2000 }).catch(() => false)) { // User has activity - check stats components await expect(page.getByTestId("total-tokens-stat")).toBeVisible(); await expect(page.getByTestId("models-used-stat")).toBeVisible(); await expect(page.getByTestId("messages-stat")).toBeVisible(); await expect(page.getByTestId("top-models-section")).toBeVisible(); } }); test("should display empty state for user with no AI activity", async ({ page, }) => { await page.goto("/admin/users"); await page.waitForSelector("[data-testid='users-table']"); // Search for a regular user (likely to have no activity) const searchInput = page.getByTestId("users-search-input"); await searchInput.fill("user10"); await searchInput.press("Enter"); await page.waitForLoadState("networkidle"); // Navigate to user detail if user exists const userRow = page .locator("[data-testid='users-table'] tbody tr") .first(); if (await userRow.isVisible({ timeout: 1000 }).catch(() => false)) { await userRow.click(); await page.waitForURL(/\/admin\/users\/.+/); // Wait for stats to load await page.waitForSelector("[data-testid='user-statistics-card']", { timeout: 10000, }); // Check for empty state indicators const emptyState = page.getByTestId("no-activity-state"); if (await emptyState.isVisible({ timeout: 2000 }).catch(() => false)) { await expect(emptyState).toBeVisible(); await expect(page.getByText(/No AI Activity Yet/i)).toBeVisible(); await expect( page.getByText(/hasn't interacted with AI models/i), ).toBeVisible(); } } }); test("should display user sessions or statistics", async ({ page }) => { // Look for Usage Statistics section const statsSection = page.getByText("Usage Statistics"); if (await statsSection.isVisible({ timeout: 2000 }).catch(() => false)) { await expect(statsSection).toBeVisible(); // Check for AI model usage text const modelUsageText = page.getByText(/AI model usage/i); if ( await modelUsageText.isVisible({ timeout: 1000 }).catch(() => false) ) { await expect(modelUsageText).toBeVisible(); } } }); }); test.describe("Role Management", () => { test("should display and update user role", async ({ page }) => { await page.goto("/admin/users"); await page.waitForSelector("[data-testid='users-table']"); await page.waitForLoadState("networkidle"); await page.locator("[data-testid='users-table'] tbody tr").nth(6).click(); await page.waitForURL(/\/admin\/users\/.+/); await page.waitForLoadState("networkidle"); // Look for the Access & Account section await expect(page.getByText("Access & Account")).toBeVisible(); // Check that Roles section exists await expect(page.getByText("Roles").first()).toBeVisible(); // Look for Edit Roles button const editRolesButton = page.getByRole("button", { name: "Edit Roles" }); if ( await editRolesButton.isVisible({ timeout: 2000 }).catch(() => false) ) { await editRolesButton.click(); // Role selection dialog should open const dialog = page.getByRole("dialog"); await expect(dialog).toBeVisible(); // Find the currently selected radio button using Radix state const currentSelectedRadio = page.locator( "[data-testid^='role-radio-'][data-state='checked']", ); await expect(currentSelectedRadio).toBeVisible(); // Get the value of the currently selected radio const currentSelectedValue = await currentSelectedRadio.getAttribute("value"); // Get the specific original radio button by its data-testid const originalRadio = page.locator( `[data-testid='role-radio-${currentSelectedValue}']`, ); // Select a different role (cycle through all three: admin, editor, user) let targetRole: string; if (currentSelectedValue === "admin") { targetRole = "editor"; } else if (currentSelectedValue === "editor") { targetRole = "user"; } else { targetRole = "admin"; } const targetRadio = page.locator( `[data-testid='role-radio-${targetRole}']`, ); // Click the target role radio button await targetRadio.click(); // Verify the new role is selected (Radix should update the state) await expect(targetRadio).toHaveAttribute("data-state", "checked"); // Verify the original role is no longer selected await expect(originalRadio).not.toHaveAttribute( "data-state", "checked", ); // Submit the form const updateButton = page.getByRole("button", { name: /Update/i }); await updateButton.click(); // Wait for dialog to close await expect(dialog).not.toBeVisible(); // Verify the role was updated on the main screen const updatedRoleBadge = page.locator( `[data-testid='role-badge-${targetRole}']`, ); await expect(updatedRoleBadge).toBeVisible(); // Verify the new role text appears in the badge const expectedRoleLabels = { admin: "Admin", editor: "Editor", user: "User", }; await expect(updatedRoleBadge).toContainText( expectedRoleLabels[targetRole as keyof typeof expectedRoleLabels], ); } }); }); test.describe("User Details", () => { test("should update user details", async ({ page }) => { await page.goto("/admin/users"); await page.waitForSelector("[data-testid='users-table']"); await page.waitForLoadState("networkidle"); await page.locator("[data-testid='users-table'] tbody tr").nth(6).click(); await page.waitForURL(/\/admin\/users\/.+/); await page.waitForLoadState("networkidle"); const newName = "Updated Test User " + Date.now(); const newEmail = `updated-${Date.now()}@test-seed.local`; // Find the name input field const nameInput = page.getByTestId("user-name-input"); await expect(nameInput).toBeVisible(); // Clear and update the name await nameInput.clear(); await nameInput.fill(newName); // Check if email field is editable const emailInput = page.getByTestId("user-email-input"); if (await emailInput.isEnabled()) { await emailInput.clear(); await emailInput.fill(newEmail); } // Submit the form const saveButton = page.getByRole("button", { name: "Save Changes" }); await saveButton.click(); // Wait for update to complete await page.waitForTimeout(2000); // Verify the name was updated in the heading await expect(page.getByRole("heading", { level: 1 })).toContainText( newName, ); await expect(page.getByTestId("user-email-input")).toHaveValue(newEmail); }); }); test.describe("User Account Status Management", () => { test("should complete ban user workflow with success toast", async ({ page, }) => { await page.goto("/admin/users"); await page.waitForSelector("[data-testid='users-table']"); // Find an active non-admin user const searchInput = page.getByTestId("users-search-input"); await searchInput.fill("user8"); await searchInput.press("Enter"); await page.waitForTimeout(500); const userRow = page .locator("[data-testid='users-table'] tbody tr") .first(); if (await userRow.isVisible({ timeout: 1000 }).catch(() => false)) { await userRow.click(); await page.waitForURL(/\/admin\/users\/.+/); // Test ban functionality if user is currently active const activeBadge = page.getByTestId("status-badge-active"); if (await activeBadge.isVisible({ timeout: 1000 }).catch(() => false)) { await activeBadge.click(); // Ban confirmation dialog should open const dialog = page.getByRole("dialog"); await expect(dialog).toBeVisible(); await expect(dialog).toContainText(/ban.*user/i); await expect(dialog).toContainText(/lose access/i); // Proceed with ban const banButton = page.getByRole("button", { name: /ban.*user/i }); await banButton.click(); // Wait for success and check status changed await page.waitForTimeout(2000); await expect(page.getByTestId("status-badge-banned")).toBeVisible(); // Verify success toast appeared (check for toast text) const toastContent = page.locator(".sonner-toast"); if ( await toastContent.isVisible({ timeout: 1000 }).catch(() => false) ) { await expect(toastContent).toContainText(/success|updated/i); } // Unban the user to restore state await page.getByTestId("status-badge-banned").click(); const unbanDialog = page.getByRole("dialog"); await expect(unbanDialog).toBeVisible(); await page.getByRole("button", { name: /unban.*user/i }).click(); await page.waitForTimeout(2000); } } }); test("should show OAuth user account type restrictions", async ({ page, }) => { await page.goto("/admin/users"); await page.waitForSelector("[data-testid='users-table']"); // Search for any user (might be OAuth) const userRow = page .locator("[data-testid='users-table'] tbody tr") .nth(2); await userRow.click(); await page.waitForURL(/\/admin\/users\/.+/); // Check account type display in security section const securitySection = page.getByText(/password management/i); await expect(securitySection).toBeVisible(); // Should show either "User has a password set" or "User signs in with OAuth only" const passwordStatus = page.getByText(/password set|oauth only/i); await expect(passwordStatus).toBeVisible(); // If OAuth only, password button should be disabled const passwordButton = page.getByTestId("update-password-button"); const oauthOnlyText = page.getByText(/oauth only/i); if (await oauthOnlyText.isVisible({ timeout: 1000 }).catch(() => false)) { await expect(passwordButton).toBeDisabled(); } }); test("should show email modification restrictions for SSO users", async ({ page, }) => { await page.goto("/admin/users"); await page.waitForSelector("[data-testid='users-table']"); const userRow = page .locator("[data-testid='users-table'] tbody tr") .first(); await userRow.click(); await page.waitForURL(/\/admin\/users\/.+/); // Check email input state const emailInput = page.getByTestId("user-email-input"); // If email is disabled, should show restriction message if (await emailInput.isDisabled({ timeout: 1000 }).catch(() => false)) { const restrictionText = page.getByText( /email cannot be modified.*sso|email.*managed.*provider/i, ); if ( await restrictionText.isVisible({ timeout: 1000 }).catch(() => false) ) { await expect(restrictionText).toBeVisible(); } } }); }); test.describe("Password Management", () => { test("should open password update dialog", async ({ page }) => { // Look for Security section and Reset Password button const securitySection = page.getByText("Security"); if ( await securitySection.isVisible({ timeout: 2000 }).catch(() => false) ) { const resetPasswordButton = page.getByRole("button", { name: /Reset Password/i, }); if ( (await resetPasswordButton .isVisible({ timeout: 1000 }) .catch(() => false)) && (await resetPasswordButton.isEnabled()) ) { await resetPasswordButton.click(); // Password dialog should open const dialog = page.getByRole("dialog"); await expect(dialog).toBeVisible(); // Close dialog await page.keyboard.press("Escape"); await expect(dialog).not.toBeVisible(); } } }); test("should complete password update with success feedback", async ({ page, }) => { await page.goto("/admin/users"); await page.waitForSelector("[data-testid='users-table']"); // Find a user with password capability const searchInput = page.getByTestId("users-search-input"); await searchInput.fill("Test User 5"); await searchInput.press("Enter"); await page.waitForTimeout(500); await page .locator("[data-testid='users-table'] tbody tr") .first() .click(); await page.waitForURL(/\/admin\/users\/.+/); await page.getByTestId("update-password-button").click(); const dialog = page.getByRole("dialog"); await expect(dialog).toBeVisible(); // Should show admin context await expect(dialog).toContainText(/update user password/i); const testPassword = "NewTestPassword123!"; await page.getByTestId("new-password-input").fill(testPassword); await page.getByTestId("confirm-password-input").fill(testPassword); // Submit password update await page.getByTestId("update-password-submit-button").click(); // Verify success toast or error handling const toastContent = page.locator(".sonner-toast"); if (await toastContent.isVisible({ timeout: 1000 }).catch(() => false)) { const toastText = await toastContent.textContent(); expect(toastText).toMatch(/password.*updated|success|error/i); } }); }); test.describe("Delete User", () => { test("should complete full delete confirmation flow with name typing", async ({ page, }) => { await page.goto("/admin/users"); await page.waitForSelector("[data-testid='users-table']"); // Search for a specific test user that we can safely test deletion on const searchInput = page.getByTestId("users-search-input"); await searchInput.press("Enter"); await page.waitForTimeout(500); await page.locator("[data-testid='users-table'] tbody tr").nth(4).click(); await page.waitForURL(/\/admin\/users\/.+/); const userName = await page.getByTestId("user-name-input").inputValue(); expect(userName).toBeTruthy(); // Click delete button await page.getByTestId("delete-user-button").click(); // Delete dialog should open const dialog = page.getByRole("dialog"); await expect(dialog).toBeVisible(); // Find confirmation input const confirmInput = page.getByPlaceholder(/type.*to confirm/i); await expect(confirmInput).toBeVisible(); // Delete button should be disabled initially const deleteButton = page.getByRole("button", { name: /delete.*user/i, }); await expect(deleteButton).toBeDisabled(); // Type wrong name await confirmInput.fill("Wrong Name"); await expect(deleteButton).toBeDisabled(); // Type correct name (extracted from page) await confirmInput.clear(); if (userName) { await confirmInput.fill(userName); // Delete button should now be enabled await expect(deleteButton).toBeEnabled(); } // Cancel instead of actually deleting await page.getByRole("button", { name: /cancel/i }).click(); await expect(dialog).not.toBeVisible(); }); test("should show proper delete confirmation text with user name", async ({ page, }) => { await page.goto("/admin/users"); await page.waitForSelector("[data-testid='users-table']"); const userRow = page .locator("[data-testid='users-table'] tbody tr") .nth(4); await userRow.click(); await page.waitForURL(/\/admin\/users\/.+/); // Get user name const userName = await page.getByTestId("user-name-input").inputValue(); // Open delete dialog await page.getByTestId("delete-user-button").click(); const dialog = page.getByRole("dialog"); await expect(dialog).toBeVisible(); // Dialog should contain the user's name in multiple places if (userName) { const dialogText = await dialog.textContent(); const nameOccurrences = ( dialogText?.match(new RegExp(userName, "gi")) || [] ).length; expect(nameOccurrences).toBeGreaterThanOrEqual(2); // Name should appear in description and confirmation } await page.getByRole("button", { name: /cancel/i }).click(); }); }); test.describe("Role Management", () => { test("admin cannot update their own role", async ({ page }) => { // Navigate to admin's own profile await page.goto("/admin/users"); await page.waitForLoadState("networkidle"); // Look for search input or find admin user in the table const searchInput = page.getByPlaceholder(/search/i); if (await searchInput.isVisible({ timeout: 2000 }).catch(() => false)) { await searchInput.fill("admin@test-seed.local"); await searchInput.press("Enter"); await page.waitForTimeout(1000); } // Click on admin user row const adminRow = page .locator("table tbody tr") .filter({ hasText: "admin@test-seed.local" }); if (await adminRow.isVisible({ timeout: 2000 }).catch(() => false)) { await adminRow.click(); await page.waitForURL(/\/admin\/users\/.+/); await page.waitForLoadState("networkidle"); } // Try to change role const editRolesButton = page.getByRole("button", { name: "Edit Roles" }); // Button should be disabled or show error for self-editing if ( (await editRolesButton .isVisible({ timeout: 2000 }) .catch(() => false)) && (await editRolesButton.isEnabled()) ) { await editRolesButton.click(); // If dialog opens, try to change role const dialog = page.getByRole("dialog"); if (await dialog.isVisible({ timeout: 1000 }).catch(() => false)) { const userOption = page.getByRole("radio", { name: /User/i }); if ( await userOption.isVisible({ timeout: 1000 }).catch(() => false) ) { await userOption.click(); const updateButton = page.getByRole("button", { name: /update/i }); await updateButton.click(); // Wait for any error message await page.waitForTimeout(2000); } } } }); }); test.describe("Account Status Management", () => { test("should ban and unban user and display banned status", async ({ page, }) => { // Use the current user (already loaded from beforeEach) // Look for the Account Status section const statusSection = page.getByText("Account Status"); if (await statusSection.isVisible({ timeout: 2000 }).catch(() => false)) { // Find the status badge (should be "Active" initially) const activeBadge = page.locator("[data-testid='status-badge-active']"); if (await activeBadge.isVisible({ timeout: 2000 }).catch(() => false)) { // Click to ban the user await activeBadge.click(); // Ban confirmation dialog should open const dialog = page.getByRole("dialog"); await expect(dialog).toBeVisible(); // Confirm the ban const confirmButton = page.getByRole("button", { name: /ban/i }); await confirmButton.click(); // Wait for dialog to close and status to update await expect(dialog).not.toBeVisible(); // Verify banned status is displayed const bannedBadge = page.locator( "[data-testid='status-badge-banned']", ); await expect(bannedBadge).toBeVisible(); await expect(bannedBadge).toContainText(/banned/i); // Unban the user to clean up await bannedBadge.click(); // Unban confirmation dialog should open const unbanDialog = page.getByRole("dialog"); await expect(unbanDialog).toBeVisible(); // Confirm the unban const unbanButton = page.getByRole("button", { name: /unban/i }); await unbanButton.click(); // Wait for dialog to close and status to revert await expect(unbanDialog).not.toBeVisible(); // Verify active status is restored await expect(activeBadge).toBeVisible(); await expect(activeBadge).toContainText(/active/i); } } }); }); test.describe("Form Validation and Error Handling", () => { test("should handle user update form validation", async ({ page }) => { await page.goto("/admin/users"); await page.waitForSelector("[data-testid='users-table']"); const userRow = page .locator("[data-testid='users-table'] tbody tr") .first(); await userRow.click(); await page.waitForURL(/\/admin\/users\/.+/); // Try to save with invalid email const emailInput = page.getByTestId("user-email-input"); if (await emailInput.isEnabled({ timeout: 1000 }).catch(() => false)) { await emailInput.clear(); await emailInput.fill("invalid-email"); const saveButton = page.getByTestId("save-changes-button"); await saveButton.click(); // Should show validation error (either client-side or server-side) await page.waitForTimeout(1000); // Form validation should prevent submission or show error // (Implementation may vary - could be HTML5 validation or custom) } }); test("should handle empty name validation", async ({ page }) => { await page.goto("/admin/users"); await page.waitForSelector("[data-testid='users-table']"); const userRow = page .locator("[data-testid='users-table'] tbody tr") .first(); await userRow.click(); await page.waitForURL(/\/admin\/users\/.+/); // Try to save with empty name const nameInput = page.getByTestId("user-name-input"); await nameInput.clear(); const saveButton = page.getByTestId("save-changes-button"); await saveButton.click(); // Should show validation error or prevent submission await page.waitForTimeout(1000); }); }); });