diff --git a/tests/e2e/gui-settings-apikeys.spec.ts b/tests/e2e/gui-settings-apikeys.spec.ts new file mode 100644 index 00000000..2ff41359 --- /dev/null +++ b/tests/e2e/gui-settings-apikeys.spec.ts @@ -0,0 +1,205 @@ +import { expect, test } from "./helpers"; + +// --------------------------------------------------------------------------- +// Settings Dialog -- API Keys tab (full coverage) +// --------------------------------------------------------------------------- + +test.describe("GUI Settings - API Keys Tab", () => { + test("navigates to API Keys tab and shows heading", async ({ loggedInPage: page }) => { + await page.locator("aside").getByText("Settings").click(); + await page.getByRole("button", { name: /api keys/i }).click(); + + await expect(page.locator("h3").filter({ hasText: "API Keys" })).toBeVisible(); + await expect(page.getByText("Manage API keys for programmatic access")).toBeVisible(); + }); + + test("shows Generate API Key button and name input", async ({ loggedInPage: page }) => { + await page.locator("aside").getByText("Settings").click(); + await page.getByRole("button", { name: /api keys/i }).click(); + + await expect(page.getByPlaceholder("Key name (optional)")).toBeVisible(); + await expect(page.getByRole("button", { name: /generate api key/i })).toBeVisible(); + }); + + test("generating an API key displays the key with copy button", async ({ + loggedInPage: page, + }) => { + await page.locator("aside").getByText("Settings").click(); + await page.getByRole("button", { name: /api keys/i }).click(); + + const keyName = `apiKeyTest-${Date.now()}`; + await page.getByPlaceholder("Key name (optional)").fill(keyName); + await page.getByRole("button", { name: /generate api key/i }).click(); + + // The key should appear in a code element + const keyDisplay = page.locator("code.font-mono"); + await expect(keyDisplay).toBeVisible({ timeout: 5_000 }); + + // Copy button should be visible next to the key + await expect(page.locator("button[title='Copy']")).toBeVisible(); + + // The "Store this key" warning should be visible + await expect(page.getByText("Store this key securely")).toBeVisible(); + + // Clean up: delete the key we just created + const deleteBtn = page.locator("button[title='Delete key']").first(); + if (await deleteBtn.isVisible()) { + page.on("dialog", (d) => d.accept()); + await deleteBtn.click(); + } + }); + + test("generated key appears in Existing Keys list", async ({ loggedInPage: page }) => { + await page.locator("aside").getByText("Settings").click(); + await page.getByRole("button", { name: /api keys/i }).click(); + + const keyName = `listTest-${Date.now()}`; + await page.getByPlaceholder("Key name (optional)").fill(keyName); + await page.getByRole("button", { name: /generate api key/i }).click(); + + // Wait for key to appear + await expect(page.locator("code.font-mono")).toBeVisible({ timeout: 5_000 }); + + // The key name should appear in the "Existing Keys" section + await expect(page.getByText("Existing Keys")).toBeVisible(); + await expect(page.getByText(keyName)).toBeVisible(); + + // Clean up + page.on("dialog", (d) => d.accept()); + await page.locator("button[title='Delete key']").first().click(); + await page.waitForTimeout(500); + }); + + test("permission scoping toggle reveals checkboxes", async ({ loggedInPage: page }) => { + await page.locator("aside").getByText("Settings").click(); + await page.getByRole("button", { name: /api keys/i }).click(); + + const scopingToggle = page.getByText("Restrict permissions (optional)"); + await expect(scopingToggle).toBeVisible(); + + await scopingToggle.click(); + + // After expanding, the collapse text and permission checkboxes appear + await expect(page.getByText("Remove permission scoping")).toBeVisible(); + await expect(page.locator("input[type='checkbox']").first()).toBeVisible(); + await expect(page.getByText("tools:use")).toBeVisible(); + }); + + test("permission scoping checkboxes can be toggled", async ({ loggedInPage: page }) => { + await page.locator("aside").getByText("Settings").click(); + await page.getByRole("button", { name: /api keys/i }).click(); + + // Open scoping panel + await page.getByText("Restrict permissions (optional)").click(); + await expect(page.getByText("Remove permission scoping")).toBeVisible(); + + // Find the first checkbox and verify it can be checked + const firstCheckbox = page.locator("input[type='checkbox']").first(); + await expect(firstCheckbox).not.toBeChecked(); + await firstCheckbox.check(); + await expect(firstCheckbox).toBeChecked(); + + // Uncheck it + await firstCheckbox.uncheck(); + await expect(firstCheckbox).not.toBeChecked(); + }); + + test("deleting an API key removes it from the list", async ({ loggedInPage: page }) => { + await page.locator("aside").getByText("Settings").click(); + await page.getByRole("button", { name: /api keys/i }).click(); + + // Create a key to delete + const keyName = `deleteTest-${Date.now()}`; + await page.getByPlaceholder("Key name (optional)").fill(keyName); + await page.getByRole("button", { name: /generate api key/i }).click(); + await expect(page.locator("code.font-mono")).toBeVisible({ timeout: 5_000 }); + + // Confirm the key appears + await expect(page.getByText(keyName)).toBeVisible(); + + // Delete it + page.on("dialog", (d) => d.accept()); + await page.locator("button[title='Delete key']").first().click(); + + // Key name should disappear + await expect(page.getByText(keyName)).not.toBeVisible({ timeout: 5_000 }); + }); + + test("expiration date input is available", async ({ loggedInPage: page }) => { + await page.locator("aside").getByText("Settings").click(); + await page.getByRole("button", { name: /api keys/i }).click(); + + await expect(page.getByText("Expires:")).toBeVisible(); + await expect(page.locator("input[type='datetime-local']")).toBeVisible(); + }); + + test("expiration date Clear button works", async ({ loggedInPage: page }) => { + await page.locator("aside").getByText("Settings").click(); + await page.getByRole("button", { name: /api keys/i }).click(); + + const dateInput = page.locator("input[type='datetime-local']"); + await dateInput.fill("2030-12-31T23:59"); + + // Clear button should appear after setting a date + const clearBtn = page.getByRole("button", { name: /clear/i }); + await expect(clearBtn).toBeVisible(); + await clearBtn.click(); + + // Input should be empty again + await expect(dateInput).toHaveValue(""); + }); + + test("empty state shows no keys message or existing keys list", async ({ + loggedInPage: page, + }) => { + await page.locator("aside").getByText("Settings").click(); + await page.getByRole("button", { name: /api keys/i }).click(); + + // Wait for the API Keys heading to confirm the section loaded + await expect(page.locator("h3").filter({ hasText: "API Keys" })).toBeVisible(); + // Wait for Generate button to appear (confirms loading is complete) + await expect(page.getByRole("button", { name: /generate api key/i })).toBeVisible(); + + // If no keys exist, the empty message should be shown; otherwise existing keys list + const noKeysText = page.getByText("No API keys yet. Generate one to get started."); + const existingKeysHeader = page.getByText("Existing Keys"); + + // Wait briefly for state to settle after loading + await page.waitForTimeout(500); + const noKeysVisible = await noKeysText.isVisible().catch(() => false); + const existingVisible = await existingKeysHeader.isVisible().catch(() => false); + + // One of the two states must be true + expect(noKeysVisible || existingVisible).toBe(true); + }); + + test("generating a key with scoped permissions creates a scoped key", async ({ + loggedInPage: page, + }) => { + await page.locator("aside").getByText("Settings").click(); + await page.getByRole("button", { name: /api keys/i }).click(); + + // Set a key name + const keyName = `scopedKey-${Date.now()}`; + await page.getByPlaceholder("Key name (optional)").fill(keyName); + + // Open scoping and select a permission + await page.getByText("Restrict permissions (optional)").click(); + await expect(page.getByText("Remove permission scoping")).toBeVisible(); + + // Check the first permission checkbox + await page.locator("input[type='checkbox']").first().check(); + + // Generate the key + await page.getByRole("button", { name: /generate api key/i }).click(); + await expect(page.locator("code.font-mono")).toBeVisible({ timeout: 5_000 }); + + // The scoped key should show its permissions in the Existing Keys list + await expect(page.getByText("Scoped:")).toBeVisible(); + + // Clean up + page.on("dialog", (d) => d.accept()); + await page.locator("button[title='Delete key']").first().click(); + await page.waitForTimeout(500); + }); +}); diff --git a/tests/e2e/gui-settings-general.spec.ts b/tests/e2e/gui-settings-general.spec.ts index 1d42aae4..d5d86274 100644 --- a/tests/e2e/gui-settings-general.spec.ts +++ b/tests/e2e/gui-settings-general.spec.ts @@ -201,4 +201,219 @@ test.describe("GUI Settings - About Tab", () => { await expect(page.getByRole("link", { name: /github repository/i })).toBeVisible(); await expect(page.getByRole("link", { name: /documentation/i })).toBeVisible(); }); + + test("shows API Reference (Swagger) link", async ({ loggedInPage: page }) => { + await page.locator("aside").getByText("Settings").click(); + await page.getByRole("button", { name: /about/i }).click(); + + await expect(page.getByRole("link", { name: /api reference/i })).toBeVisible(); + }); + + test("version displays a semver string", async ({ loggedInPage: page }) => { + await page.locator("aside").getByText("Settings").click(); + await page.getByRole("button", { name: /about/i }).click(); + + // Version text should match semver format (e.g., 1.2.3) + const versionEl = page.locator(".font-mono").filter({ hasText: /^\d+\.\d+\.\d+/ }); + await expect(versionEl).toBeVisible(); + }); + + test("about section has the Links heading", async ({ loggedInPage: page }) => { + await page.locator("aside").getByText("Settings").click(); + await page.getByRole("button", { name: /about/i }).click(); + + await expect(page.getByText("Links")).toBeVisible(); + }); +}); + +test.describe("GUI Settings - AI Features Tab", () => { + test("displays AI Features heading and description", async ({ loggedInPage: page }) => { + await page.locator("aside").getByText("Settings").click(); + await page.getByRole("button", { name: /ai features/i }).click(); + + await expect(page.locator("h3").filter({ hasText: "AI Features" })).toBeVisible(); + await expect(page.getByText("Manage AI model bundles")).toBeVisible(); + }); + + test("shows Install All button", async ({ loggedInPage: page }) => { + await page.locator("aside").getByText("Settings").click(); + await page.getByRole("button", { name: /ai features/i }).click(); + + await expect(page.getByRole("button", { name: /install all/i })).toBeVisible(); + }); + + test("lists feature bundles with install status", async ({ loggedInPage: page }) => { + await page.locator("aside").getByText("Settings").click(); + await page.getByRole("button", { name: /ai features/i }).click(); + + // Each bundle card is rendered within a bordered div + const bundleCards = page.locator(".rounded-lg.border.border-border.p-4"); + + // At least one bundle should be present + const count = await bundleCards.count(); + expect(count).toBeGreaterThan(0); + + // Each card should have a name and a status indicator (Installed or Not installed or Install button) + const firstCard = bundleCards.first(); + const hasStatus = + (await firstCard + .getByText("Installed") + .isVisible() + .catch(() => false)) || + (await firstCard + .getByText("Not installed") + .isVisible() + .catch(() => false)) || + (await firstCard + .getByRole("button", { name: /install/i }) + .isVisible() + .catch(() => false)); + expect(hasStatus).toBe(true); + }); + + test("bundles show estimated size", async ({ loggedInPage: page }) => { + await page.locator("aside").getByText("Settings").click(); + await page.getByRole("button", { name: /ai features/i }).click(); + + // Each bundle should display an estimated size like (~XXX MB) + await expect(page.getByText(/~\d+/).first()).toBeVisible(); + }); +}); + +test.describe("GUI Settings - Tools Tab (deep)", () => { + test("all tools are listed with enable/disable toggles", async ({ loggedInPage: page }) => { + await page.locator("aside").getByText("Settings").click(); + await page.getByRole("button", { name: /tools/i }).click(); + + await expect(page.locator("h3").filter({ hasText: "Tools" }).first()).toBeVisible(); + // Wait for tools to finish loading (disabled counter appears when loaded) + await expect(page.getByText(/\d+ tools? disabled/)).toBeVisible({ timeout: 5_000 }); + + // Each tool row has an enable/disable toggle (w-11 h-6 rounded-full) + const toolToggles = page.locator("button.w-11.h-6"); + const count = await toolToggles.count(); + // Should have many tools (SnapOtter has 47) + expect(count).toBeGreaterThan(10); + }); + + test("toggling a tool changes the disabled counter", async ({ loggedInPage: page }) => { + await page.locator("aside").getByText("Settings").click(); + await page.getByRole("button", { name: /tools/i }).click(); + + // Read initial disabled count + const counterText = page.getByText(/\d+ tools? disabled/); + await expect(counterText).toBeVisible({ timeout: 5_000 }); + const initialText = await counterText.textContent(); + const initialCount = parseInt(initialText?.match(/(\d+)/)?.[1] || "0", 10); + + // Click the first tool toggle to change its state + const firstToggle = page.locator("button.w-11.h-6").first(); + await firstToggle.click(); + + // The counter should change by 1 (either +1 or -1) + const updatedText = await counterText.textContent(); + const updatedCount = parseInt(updatedText?.match(/(\d+)/)?.[1] || "0", 10); + expect(Math.abs(updatedCount - initialCount)).toBe(1); + + // Revert the toggle to not affect other tests + await firstToggle.click(); + }); + + test("search filters tools in settings dialog", async ({ loggedInPage: page }) => { + await page.locator("aside").getByText("Settings").click(); + await page.getByRole("button", { name: /tools/i }).click(); + + // Wait for the tools to load + await expect(page.getByText(/\d+ tools? disabled/)).toBeVisible({ timeout: 5_000 }); + + // The Settings dialog content area has the search input + const dialogContent = page.locator(".flex-1.overflow-y-auto"); + const searchInput = dialogContent.getByPlaceholder("Search tools..."); + await expect(searchInput).toBeVisible(); + + // Search for a tool name that likely exists + await searchInput.fill("Resize"); + + // Should show a filtered subset; the "Resize" tool should be visible + await expect(dialogContent.getByText("Resize").first()).toBeVisible(); + + // Search for something that does not exist + await searchInput.fill("zzzznonexistenttool"); + await expect(dialogContent.getByText("No tools match your search.")).toBeVisible(); + + // Clear search restores all tools + await searchInput.fill(""); + const toolToggles = page.locator("button.w-11.h-6"); + const count = await toolToggles.count(); + expect(count).toBeGreaterThan(10); + }); +}); + +test.describe("GUI Settings - Product Analytics Tab (deep)", () => { + test("displays description about anonymous data and image privacy", async ({ + loggedInPage: page, + }) => { + await page.locator("aside").getByText("Settings").click(); + await page.getByRole("button", { name: /product analytics/i }).click(); + + await expect(page.getByText(/share anonymous usage data/i)).toBeVisible(); + await expect(page.getByText(/images never leave your machine/i)).toBeVisible(); + }); + + test("shows either consent toggle or admin-disabled message", async ({ loggedInPage: page }) => { + await page.locator("aside").getByText("Settings").click(); + await page.getByRole("button", { name: /product analytics/i }).click(); + + // Either the toggle button is present, or the admin-disabled message is shown + const toggle = page.locator("button.rounded-full"); + const disabledMsg = page.getByText(/has been disabled by the server administrator/i); + + const toggleVisible = await toggle.isVisible().catch(() => false); + const disabledVisible = await disabledMsg.isVisible().catch(() => false); + + // One of the two states must be true + expect(toggleVisible || disabledVisible).toBe(true); + }); +}); + +test.describe("GUI Settings - Save and Persistence", () => { + test("System Settings save persists after dialog close and reopen", async ({ + loggedInPage: page, + }) => { + // Navigate to System Settings tab + await page.locator("aside").getByText("Settings").click(); + await page.getByRole("button", { name: /system settings/i }).click(); + await expect(page.getByText("App Name")).toBeVisible({ timeout: 5_000 }); + + // Wait for the input to load + const appNameInput = page.locator("input[type='text']").first(); + await expect(appNameInput).toBeVisible(); + const originalName = await appNameInput.inputValue(); + const testName = originalName === "SnapOtter" ? "TestApp" : "SnapOtter"; + + // Change the App Name + await appNameInput.fill(testName); + + // Save + await page.getByRole("button", { name: /save settings/i }).click(); + await expect(page.getByText("Settings saved.")).toBeVisible({ timeout: 5_000 }); + + // Close the dialog + await page.keyboard.press("Escape"); + await expect(page.locator("h2").filter({ hasText: "Settings" })).not.toBeVisible(); + + // Reopen the dialog and go to System Settings + await page.locator("aside").getByText("Settings").click(); + await page.getByRole("button", { name: /system settings/i }).click(); + await expect(page.getByText("App Name")).toBeVisible(); + + // Verify the setting persisted + const persistedName = await page.locator("input[type='text']").first().inputValue(); + expect(persistedName).toBe(testName); + + // Restore original name + await page.locator("input[type='text']").first().fill(originalName); + await page.getByRole("button", { name: /save settings/i }).click(); + await expect(page.getByText("Settings saved.")).toBeVisible({ timeout: 5_000 }); + }); }); diff --git a/tests/e2e/gui-settings-people.spec.ts b/tests/e2e/gui-settings-people.spec.ts index fb730075..1e87e142 100644 --- a/tests/e2e/gui-settings-people.spec.ts +++ b/tests/e2e/gui-settings-people.spec.ts @@ -203,6 +203,83 @@ test.describe("GUI Settings - People Tab", () => { timeout: 5_000, }); }); + + test("deleting a non-admin user removes them from the table", async ({ loggedInPage: page }) => { + const username = `guidelete-${UID}`; + const adminToken = await getAdminToken(); + + try { + // Create user via API first + await fetch(`${API}/api/auth/register`, { + method: "POST", + headers: authJson(adminToken), + body: JSON.stringify({ username, password: "TestPass123!", role: "user" }), + }); + + await page.locator("aside").getByText("Settings").click(); + await page.getByRole("button", { name: /people/i }).click(); + await page.waitForTimeout(500); + + // Verify the user appears + await expect(page.getByText(username)).toBeVisible({ timeout: 5_000 }); + + // Open the actions menu for the test user (last Actions button) + await page.getByTitle("Actions").last().click(); + + // Accept the confirm dialog and click Delete User + page.on("dialog", (d) => d.accept()); + await page.getByText("Delete User").click(); + + // User should be removed from the list + await expect(page.getByText(username)).not.toBeVisible({ timeout: 5_000 }); + } finally { + await cleanupUsersByPrefix(adminToken, "guidelete-"); + } + }); + + test("cannot demote your own admin role via Edit Role / Team", async ({ loggedInPage: page }) => { + await page.locator("aside").getByText("Settings").click(); + await page.getByRole("button", { name: /people/i }).click(); + await page.waitForTimeout(500); + + // The admin row should have an Actions button + await page.getByTitle("Actions").first().click(); + await page.getByText("Edit Role / Team").click(); + + // The edit form should appear with current role + await expect(page.getByText(/edit admin/i)).toBeVisible(); + + // Change admin role to user + const roleSelect = page.locator("form select").first(); + await roleSelect.selectOption("user"); + + // Click Save + await page.getByRole("button", { name: /^save$/i }).click(); + + // Should show an error about not being able to remove own admin role + await expect(page.getByText(/cannot remove your own admin role|failed/i).first()).toBeVisible({ + timeout: 5_000, + }); + }); + + test("Reset Password opens the reset form for a user", async ({ loggedInPage: page }) => { + await page.locator("aside").getByText("Settings").click(); + await page.getByRole("button", { name: /people/i }).click(); + await page.waitForTimeout(500); + + // Open the actions menu on the first user row + await page.getByTitle("Actions").first().click(); + await page.getByText("Reset Password").click(); + + // The reset form should appear + await expect(page.getByText(/reset password for/i)).toBeVisible(); + await expect(page.getByPlaceholder(/new password/i)).toBeVisible(); + await expect(page.getByRole("button", { name: /reset password/i })).toBeVisible(); + + // Cancel closes the form + await page.getByRole("button", { name: /cancel/i }).click(); + await expect(page.getByText(/reset password for/i)).not.toBeVisible(); + }); }); test.describe("GUI Settings - Teams Tab", () => { diff --git a/tests/e2e/gui-settings-rbac.spec.ts b/tests/e2e/gui-settings-rbac.spec.ts index 79e5e357..4e95b03a 100644 --- a/tests/e2e/gui-settings-rbac.spec.ts +++ b/tests/e2e/gui-settings-rbac.spec.ts @@ -112,7 +112,7 @@ async function deleteUser(adminToken: string, username: string): Promise { // about - none base.describe("RBAC Settings Visibility - Admin", () => { - base.use({ storageState: "test-results/.auth/user.json" }); + base.use({ storageState: ".playwright/.auth/user.json" }); base.test("admin sees all settings tabs including admin-only ones", async ({ page }) => { await page.goto("/"); @@ -134,6 +134,37 @@ base.describe("RBAC Settings Visibility - Admin", () => { await expect(page.getByRole("button", { name: /audit log/i })).toBeVisible(); await expect(page.getByRole("button", { name: /ai features/i })).toBeVisible(); }); + + base.test("admin can navigate to People tab and see user table", async ({ page }) => { + await page.goto("/"); + await page.locator("aside").getByText("Settings").click(); + await page.getByRole("button", { name: /people/i }).click(); + + await expect(page.getByText(/\d+ users?/)).toBeVisible({ timeout: 5_000 }); + await expect(page.getByText("admin").first()).toBeVisible(); + }); + + base.test("admin can navigate to Audit Log tab and see entries", async ({ page }) => { + await page.goto("/"); + await page.locator("aside").getByText("Settings").click(); + await page.getByRole("button", { name: /audit log/i }).click(); + + await expect(page.locator("h3").filter({ hasText: "Audit Log" })).toBeVisible(); + // Filter dropdown should be present + await expect( + page.locator("select").filter({ has: page.locator("option[value='']") }), + ).toBeVisible(); + }); + + base.test("admin sees all 12 nav items", async ({ page }) => { + await page.goto("/"); + await page.locator("aside").getByText("Settings").click(); + + // Count the navigation buttons in the settings dialog sidebar + const navButtons = page.locator(".w-48 button"); + const count = await navButtons.count(); + expect(count).toBe(12); + }); }); base.describe("RBAC Settings Visibility - Editor", () => { @@ -182,6 +213,33 @@ base.describe("RBAC Settings Visibility - Editor", () => { await expect(page.getByRole("button", { name: /ai features/i })).not.toBeVisible(); }, ); + + base.test("editor sees exactly 6 nav items", async ({ page }) => { + await login(page, EDITOR_USER, EDITOR_PASS); + await page.locator("aside").getByText("Settings").click(); + await expect(page.getByRole("button", { name: /general/i })).toBeVisible(); + + const navButtons = page.locator(".w-48 button"); + const count = await navButtons.count(); + expect(count).toBe(6); + }); + + base.test("editor can access Security tab and see change password form", async ({ page }) => { + await login(page, EDITOR_USER, EDITOR_PASS); + await page.locator("aside").getByText("Settings").click(); + await page.getByRole("button", { name: /security/i }).click(); + + await expect(page.getByText("Change Password").first()).toBeVisible(); + await expect(page.getByPlaceholder("Current Password")).toBeVisible(); + }); + + base.test("editor can access API Keys tab and generate a key", async ({ page }) => { + await login(page, EDITOR_USER, EDITOR_PASS); + await page.locator("aside").getByText("Settings").click(); + await page.getByRole("button", { name: /api keys/i }).click(); + + await expect(page.getByRole("button", { name: /generate api key/i })).toBeVisible(); + }); }); base.describe("RBAC Settings Visibility - User", () => { @@ -225,4 +283,32 @@ base.describe("RBAC Settings Visibility - User", () => { await expect(page.getByRole("button", { name: /ai features/i })).not.toBeVisible(); }, ); + + base.test("user sees exactly 6 nav items", async ({ page }) => { + await login(page, USER_USER, USER_PASS); + await page.locator("aside").getByText("Settings").click(); + await expect(page.getByRole("button", { name: /general/i })).toBeVisible(); + + const navButtons = page.locator(".w-48 button"); + const count = await navButtons.count(); + expect(count).toBe(6); + }); + + base.test("user can access About tab and see version", async ({ page }) => { + await login(page, USER_USER, USER_PASS); + await page.locator("aside").getByText("Settings").click(); + await page.getByRole("button", { name: /about/i }).click(); + + await expect(page.locator("h3").filter({ hasText: "About" })).toBeVisible(); + await expect(page.getByText("Version:")).toBeVisible(); + }); + + base.test("user General tab shows correct username and role", async ({ page }) => { + await login(page, USER_USER, USER_PASS); + await page.locator("aside").getByText("Settings").click(); + + // General is the default tab; should show the user's username and role + await expect(page.getByText(USER_USER)).toBeVisible({ timeout: 5_000 }); + await expect(page.getByText("user").first()).toBeVisible(); + }); }); diff --git a/tests/e2e/gui-settings-security.spec.ts b/tests/e2e/gui-settings-security.spec.ts index b19db896..607e24f2 100644 --- a/tests/e2e/gui-settings-security.spec.ts +++ b/tests/e2e/gui-settings-security.spec.ts @@ -47,6 +47,30 @@ test.describe("GUI Settings - Security Tab", () => { await expect(currentPwInput).toHaveAttribute("type", "text"); }); + + test("short password shows validation error", async ({ loggedInPage: page }) => { + await page.locator("aside").getByText("Settings").click(); + await page.getByRole("button", { name: /security/i }).click(); + + await page.getByPlaceholder("Current Password").fill("admin"); + await page.getByPlaceholder("New Password").first().fill("ab"); + await page.getByPlaceholder("Confirm New Password").fill("ab"); + + await page.getByRole("button", { name: /change password/i }).click(); + + // Should show validation error about minimum length + await expect(page.getByText(/at least 4 characters/i)).toBeVisible({ timeout: 5_000 }); + }); + + test("security section shows login attempt limit reference", async ({ loggedInPage: page }) => { + await page.locator("aside").getByText("Settings").click(); + await page.getByRole("button", { name: /security/i }).click(); + + await expect(page.getByText(/login attempt limits/i)).toBeVisible(); + await expect( + page.getByText("Login attempt limits can be configured in System Settings."), + ).toBeVisible(); + }); }); test.describe("GUI Settings - API Keys Tab", () => { diff --git a/tests/e2e/gui-settings-tools.spec.ts b/tests/e2e/gui-settings-tools.spec.ts index 37f8c304..948c509a 100644 --- a/tests/e2e/gui-settings-tools.spec.ts +++ b/tests/e2e/gui-settings-tools.spec.ts @@ -39,6 +39,35 @@ test.describe("GUI Settings - Tools Tab", () => { }); }); +test.describe("GUI Settings - Tools Tab (additional)", () => { + test("each category has a heading", async ({ loggedInPage: page }) => { + await page.locator("aside").getByText("Settings").click(); + await page.getByRole("button", { name: /tools/i }).click(); + + await expect(page.locator("h3").filter({ hasText: "Tools" }).first()).toBeVisible(); + // Wait for tools to finish loading + await expect(page.getByText(/\d+ tools? disabled/)).toBeVisible({ timeout: 5_000 }); + + // Category headings are h4 elements inside the dialog content + const dialogContent = page.locator(".flex-1.overflow-y-auto"); + const headings = dialogContent.locator("h4"); + const count = await headings.count(); + expect(count).toBeGreaterThanOrEqual(3); + }); + + test("tools show both name and description", async ({ loggedInPage: page }) => { + await page.locator("aside").getByText("Settings").click(); + await page.getByRole("button", { name: /tools/i }).click(); + + // Wait for tools to load + await expect(page.getByText(/\d+ tools? disabled/)).toBeVisible({ timeout: 5_000 }); + + // Verify the Resize tool is listed with its description + const dialogContent = page.locator(".flex-1.overflow-y-auto"); + await expect(dialogContent.getByText("Resize").first()).toBeVisible(); + }); +}); + test.describe("GUI Settings - Product Analytics Tab", () => { test("displays analytics consent section", async ({ loggedInPage: page }) => { await page.locator("aside").getByText("Settings").click();