Files
SnapOtter/tests/e2e/gui-settings-expanded.spec.ts
T
SnapOtter 06d1822491 test: expand test coverage across all layers (+1,157 tests)
Fix 2 failing unit tests (landing hero text mismatch) and broken
coverage tooling (brace-expansion v5 override breaking minimatch).
Add ~1,097 new test cases via 14-agent parallel expansion:

- Unit: +290 tests (AI bridge, image-engine, stores, API helpers)
- Integration: +504 tests (all tools, cross-format matrix, adversarial)
- E2E: +363 tests (navigation, tool UI, batch/pipeline, settings,
  visual regression, accessibility, performance, cross-browser)

Total: 4,223 unit + 6,057 integration + 1,563 E2E = 11,843 tests
2026-06-06 19:37:29 +08:00

1507 lines
55 KiB
TypeScript

import { test as base, expect } from "@playwright/test";
import { login, openSettings, test } from "./helpers";
const API = process.env.API_URL || "http://localhost:13490";
/** Auth + JSON content-type (POST, PUT). */
function authJson(token: string): Record<string, string> {
return { Authorization: `Bearer ${token}`, "Content-Type": "application/json" };
}
/** Auth header only (GET, DELETE). */
function authOnly(token: string): Record<string, string> {
return { Authorization: `Bearer ${token}` };
}
async function getAdminToken(): Promise<string> {
const res = await fetch(`${API}/api/auth/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username: "admin", password: "admin" }),
});
const data = await res.json();
return data.token;
}
/** Delete all non-admin test users by prefix. */
async function cleanupUsersByPrefix(adminToken: string, prefix: string): Promise<void> {
const listRes = await fetch(`${API}/api/auth/users`, {
headers: authOnly(adminToken),
});
if (!listRes.ok) return;
const { users } = await listRes.json();
for (const u of users) {
if (u.username.startsWith(prefix)) {
await fetch(`${API}/api/auth/users/${u.id}`, {
method: "DELETE",
headers: authOnly(adminToken),
});
}
}
}
/** Delete test teams by prefix. */
async function cleanupTeamsByPrefix(adminToken: string, prefix: string): Promise<void> {
const listRes = await fetch(`${API}/api/v1/teams`, {
headers: authOnly(adminToken),
});
if (!listRes.ok) return;
const { teams } = await listRes.json();
for (const t of teams) {
if (t.name.startsWith(prefix)) {
await fetch(`${API}/api/v1/teams/${t.id}`, {
method: "DELETE",
headers: authOnly(adminToken),
});
}
}
}
/** Delete custom roles by prefix. */
async function cleanupRolesByPrefix(adminToken: string, prefix: string): Promise<void> {
const listRes = await fetch(`${API}/api/v1/roles`, {
headers: authOnly(adminToken),
});
if (!listRes.ok) return;
const { roles } = await listRes.json();
for (const r of roles) {
if (r.name.startsWith(prefix)) {
await fetch(`${API}/api/v1/roles/${r.id}`, {
method: "DELETE",
headers: authOnly(adminToken),
});
}
}
}
/**
* Create a user with a given role and clear mustChangePassword
* so the browser login redirects to "/" instead of "/change-password".
*/
async function createReadyUser(
adminToken: string,
username: string,
password: string,
role: string,
): Promise<void> {
const createRes = await fetch(`${API}/api/auth/register`, {
method: "POST",
headers: authJson(adminToken),
body: JSON.stringify({ username, password, role }),
});
if (createRes.status !== 201 && createRes.status !== 409) {
throw new Error(`Failed to create user ${username}: ${createRes.status}`);
}
// Login to get token, then change password to clear mustChangePassword
const loginRes = await fetch(`${API}/api/auth/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username, password }),
});
if (!loginRes.ok) throw new Error(`Login failed for ${username}: ${loginRes.status}`);
const loginData = await loginRes.json();
await fetch(`${API}/api/auth/change-password`, {
method: "POST",
headers: authJson(loginData.token),
body: JSON.stringify({ currentPassword: password, newPassword: password }),
});
// Re-login and dismiss analytics consent
const reLogin = await fetch(`${API}/api/auth/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username, password }),
});
const reLoginData = await reLogin.json();
await fetch(`${API}/api/v1/user/analytics`, {
method: "PUT",
headers: authJson(reLoginData.token),
body: JSON.stringify({ enabled: false }),
});
}
/** Delete a user by username if it exists. */
async function deleteUser(adminToken: string, username: string): Promise<void> {
const listRes = await fetch(`${API}/api/auth/users`, {
headers: authOnly(adminToken),
});
if (!listRes.ok) return;
const { users } = await listRes.json();
const found = users.find((u: { username: string }) => u.username === username);
if (found) {
await fetch(`${API}/api/auth/users/${found.id}`, {
method: "DELETE",
headers: authOnly(adminToken),
});
}
}
const UID = Date.now().toString(36);
// ===========================================================================
// SETTINGS DIALOG -- Dialog Interactions (expanded)
// ===========================================================================
test.describe("Settings Dialog - Dialog state management", () => {
test("dialog content scrolls independently of the page", async ({ loggedInPage: page }) => {
await openSettings(page);
// The content pane has overflow-y-auto
const contentPane = page.locator(".flex-1.overflow-y-auto");
await expect(contentPane).toBeVisible();
// Navigate to a section with lots of content (Tools)
await page.getByRole("button", { name: /tools/i }).click();
await expect(page.locator("h3").filter({ hasText: "Tools" }).first()).toBeVisible();
});
test("rapidly switching tabs does not cause rendering errors", async ({ loggedInPage: page }) => {
await openSettings(page);
// Quickly click through several tabs
const tabs = ["Security", "About", "General", "Tools", "API Keys", "About"];
for (const tab of tabs) {
await page.getByRole("button", { name: new RegExp(tab, "i") }).click();
}
// Final tab should render correctly
await expect(page.locator("h3").filter({ hasText: "About" })).toBeVisible();
});
test("settings dialog renders with role=dialog and aria-modal", async ({
loggedInPage: page,
}) => {
await openSettings(page);
const dialog = page.getByRole("dialog");
await expect(dialog).toBeVisible();
await expect(dialog).toHaveAttribute("aria-modal", "true");
});
test("backdrop click does not interfere with dialog controls", async ({ loggedInPage: page }) => {
await openSettings(page);
// Click a button inside the dialog -- should not close it
await page.getByRole("button", { name: /about/i }).click();
await expect(page.locator("h3").filter({ hasText: "About" })).toBeVisible();
// Dialog should still be open
await expect(page.locator("h2").filter({ hasText: "Settings" })).toBeVisible();
});
});
// ===========================================================================
// GENERAL TAB -- expanded tests
// ===========================================================================
test.describe("Settings General Tab - User info details", () => {
test("displays role in capitalized format below username", async ({ loggedInPage: page }) => {
await openSettings(page);
// The role text is displayed with capitalize class
const roleEl = page.locator("p.capitalize").first();
await expect(roleEl).toBeVisible();
const roleText = await roleEl.textContent();
expect(roleText?.toLowerCase()).toContain("admin");
});
test("General tab has user preferences description", async ({ loggedInPage: page }) => {
await openSettings(page);
// Description text should be present
await expect(page.getByText("User preferences and display settings.")).toBeVisible();
});
test("Save Settings button is disabled while saving", async ({ loggedInPage: page }) => {
await openSettings(page);
const saveBtn = page.getByRole("button", { name: /save settings/i });
await expect(saveBtn).toBeVisible();
await expect(saveBtn).toBeEnabled();
// Click save and check the button shows loading state
await saveBtn.click();
// Eventually the save completes and message appears
await expect(page.getByText("Settings saved.")).toBeVisible({ timeout: 5_000 });
});
test("General tab shows Language (locale) dropdown", async ({ loggedInPage: page }) => {
await openSettings(page);
await expect(page.getByText("Language")).toBeVisible();
// The locale select has the user's current locale as its value
const localeSelect = page.locator("select").filter({ has: page.locator("option[value='en']") });
await expect(localeSelect).toBeVisible();
});
});
// ===========================================================================
// SYSTEM SETTINGS TAB -- expanded validation
// ===========================================================================
test.describe("Settings System Settings Tab - Additional coverage", () => {
test("shows description 'Server-side configuration and limits.'", async ({
loggedInPage: page,
}) => {
await openSettings(page);
await page.getByRole("button", { name: /system settings/i }).click();
await expect(page.getByText("Server-side configuration and limits.")).toBeVisible();
});
test("File Upload Limit has min=1 attribute", async ({ loggedInPage: page }) => {
await openSettings(page);
await page.getByRole("button", { name: /system settings/i }).click();
await expect(page.getByText("File Upload Limit (MB)")).toBeVisible();
const uploadInput = page.locator("input[type='number']").first();
await expect(uploadInput).toHaveAttribute("min", "1");
});
test("Login Attempt Limit has min=1 and max=100 attributes", async ({ loggedInPage: page }) => {
await openSettings(page);
await page.getByRole("button", { name: /system settings/i }).click();
await expect(page.getByText("Login Attempt Limit")).toBeVisible();
const loginInput = page.locator("input[type='number']").nth(1);
await expect(loginInput).toHaveAttribute("min", "1");
await expect(loginInput).toHaveAttribute("max", "100");
});
test("System theme dropdown has light, dark, and system options", async ({
loggedInPage: page,
}) => {
await openSettings(page);
await page.getByRole("button", { name: /system settings/i }).click();
const themeSelect = page
.locator("select")
.filter({ has: page.locator("option[value='dark']") });
const options = await themeSelect.locator("option").allTextContents();
expect(options).toContain("Light");
expect(options).toContain("Dark");
expect(options).toContain("System");
});
test("Language dropdown contains English option", async ({ loggedInPage: page }) => {
await openSettings(page);
await page.getByRole("button", { name: /system settings/i }).click();
const langSelect = page.locator("select").filter({ has: page.locator("option[value='en']") });
await expect(langSelect).toBeVisible();
const enOption = langSelect.locator("option[value='en']");
await expect(enOption).toBeAttached();
});
});
// ===========================================================================
// SECURITY TAB -- expanded password validation flows
// ===========================================================================
test.describe("Settings Security Tab - Extended password flows", () => {
test("empty form submission is prevented by browser required validation", async ({
loggedInPage: page,
}) => {
await openSettings(page);
await page.getByRole("button", { name: /security/i }).click();
// All password inputs should have the required attribute
const currentPw = page.getByPlaceholder("Current Password");
const newPw = page.getByPlaceholder("New Password").first();
const confirmPw = page.getByPlaceholder("Confirm New Password");
await expect(currentPw).toHaveAttribute("required", "");
await expect(newPw).toHaveAttribute("required", "");
await expect(confirmPw).toHaveAttribute("required", "");
});
test("multiple password visibility toggles work independently", async ({
loggedInPage: page,
}) => {
await openSettings(page);
await page.getByRole("button", { name: /security/i }).click();
const currentPwInput = page.getByPlaceholder("Current Password");
const newPwInput = page.getByPlaceholder("New Password").first();
// Both start as password type
await expect(currentPwInput).toHaveAttribute("type", "password");
await expect(newPwInput).toHaveAttribute("type", "password");
// Toggle the first one
const toggleButtons = page.locator("form button[type='button']");
await toggleButtons.first().click();
// Only the first should change
await expect(currentPwInput).toHaveAttribute("type", "text");
await expect(newPwInput).toHaveAttribute("type", "password");
});
test("security section shows Change Password sub-heading", async ({ loggedInPage: page }) => {
await openSettings(page);
await page.getByRole("button", { name: /security/i }).click();
// The form has a sub-heading for the change password section
await expect(page.getByText("Change Password").first()).toBeVisible();
});
test("changing password to same value succeeds (admin -> admin)", async ({
loggedInPage: page,
}) => {
await openSettings(page);
await page.getByRole("button", { name: /security/i }).click();
await page.getByPlaceholder("Current Password").fill("admin");
await page.getByPlaceholder("New Password").first().fill("admin");
await page.getByPlaceholder("Confirm New Password").fill("admin");
await page.getByRole("button", { name: /change password/i }).click();
// Should show success (not "passwords do not match")
await expect(page.getByText("Password changed successfully")).toBeVisible({ timeout: 5_000 });
});
});
// ===========================================================================
// ABOUT TAB -- expanded tests
// ===========================================================================
test.describe("Settings About Tab - Extended", () => {
test("shows AGPLv3 license information", async ({ loggedInPage: page }) => {
await openSettings(page);
await page.getByRole("button", { name: /about/i }).click();
await expect(page.getByText("AGPLv3")).toBeVisible();
});
test("shows SnapOtter logo element", async ({ loggedInPage: page }) => {
await openSettings(page);
await page.getByRole("button", { name: /about/i }).click();
// The OtterLogo component renders an SVG
const logo = page.locator("svg.text-primary").first();
await expect(logo).toBeVisible();
});
test("GitHub link points to snapotter-hq repository", async ({ loggedInPage: page }) => {
await openSettings(page);
await page.getByRole("button", { name: /about/i }).click();
const githubLink = page.getByRole("link", { name: /github repository/i });
await expect(githubLink).toHaveAttribute("href", "https://github.com/snapotter-hq/snapotter");
});
test("documentation link points to docs.snapotter.com", async ({ loggedInPage: page }) => {
await openSettings(page);
await page.getByRole("button", { name: /about/i }).click();
const docsLink = page.getByRole("link", { name: /documentation/i });
await expect(docsLink).toHaveAttribute("href", "https://docs.snapotter.com/");
});
test("API Reference link points to /api/docs", async ({ loggedInPage: page }) => {
await openSettings(page);
await page.getByRole("button", { name: /about/i }).click();
const apiLink = page.getByRole("link", { name: /api reference/i });
await expect(apiLink).toHaveAttribute("href", "/api/docs");
});
test("external links open in new tab", async ({ loggedInPage: page }) => {
await openSettings(page);
await page.getByRole("button", { name: /about/i }).click();
const githubLink = page.getByRole("link", { name: /github repository/i });
await expect(githubLink).toHaveAttribute("target", "_blank");
await expect(githubLink).toHaveAttribute("rel", /noopener/);
});
test("shows License label and description", async ({ loggedInPage: page }) => {
await openSettings(page);
await page.getByRole("button", { name: /about/i }).click();
await expect(page.getByText("License:").first()).toBeVisible();
});
});
// ===========================================================================
// PEOPLE TAB -- team assignment during user creation
// ===========================================================================
test.describe("Settings People Tab - Team assignment", () => {
test("team dropdown in add form contains Default team", async ({ loggedInPage: page }) => {
await openSettings(page);
await page.getByRole("button", { name: /people/i }).click();
await page.waitForTimeout(500);
await page.getByRole("button", { name: /add members/i }).click();
await expect(page.getByPlaceholder("Username")).toBeVisible();
// The team dropdown is the second select in the form
const teamSelect = page.locator("form select").nth(1);
await expect(teamSelect).toBeVisible();
// Default team should be an option
await expect(teamSelect.locator("option")).toContainText(["Default"]);
// Cancel the form
await page.getByRole("button", { name: /cancel/i }).click();
});
test("Generate Password button populates the password field", async ({ loggedInPage: page }) => {
await openSettings(page);
await page.getByRole("button", { name: /people/i }).click();
await page.waitForTimeout(500);
await page.getByRole("button", { name: /add members/i }).click();
await expect(page.getByPlaceholder("Username")).toBeVisible();
// Click the Generate Password button
const generateBtn = page.getByRole("button", { name: /generate/i });
await expect(generateBtn).toBeVisible();
await generateBtn.click();
// The password field should now have a value (16 chars generated password)
const pwInput = page.locator("form input[type='text']").first();
const value = await pwInput.inputValue();
expect(value.length).toBeGreaterThanOrEqual(8);
// Cancel the form
await page.getByRole("button", { name: /cancel/i }).click();
});
test("creating user with specific team assigns them correctly", async ({
loggedInPage: page,
}) => {
const teamName = `exteam-${UID}`;
const username = `extuser-${UID}`;
const adminToken = await getAdminToken();
try {
// Create a team via API first
await fetch(`${API}/api/v1/teams`, {
method: "POST",
headers: authJson(adminToken),
body: JSON.stringify({ name: teamName }),
});
await openSettings(page);
await page.getByRole("button", { name: /people/i }).click();
await page.waitForTimeout(500);
await page.getByRole("button", { name: /add members/i }).click();
await page.getByPlaceholder("Username").fill(username);
await page.getByPlaceholder("Password").fill("TestPass123!");
// Select the created team
const teamSelect = page.locator("form select").nth(1);
await teamSelect.selectOption(teamName);
await page.getByRole("button", { name: /create/i }).click();
// User should appear in the table with the team name
await expect(page.getByText(username)).toBeVisible({ timeout: 5_000 });
await expect(page.getByText(teamName).first()).toBeVisible();
} finally {
await cleanupUsersByPrefix(adminToken, "extuser-");
await cleanupTeamsByPrefix(adminToken, "exteam-");
}
});
test("Add Members form shows 'New Member' heading", async ({ loggedInPage: page }) => {
await openSettings(page);
await page.getByRole("button", { name: /people/i }).click();
await page.waitForTimeout(500);
await page.getByRole("button", { name: /add members/i }).click();
await expect(page.getByText("New Member")).toBeVisible();
await page.getByRole("button", { name: /cancel/i }).click();
});
test("canceling add form resets it", async ({ loggedInPage: page }) => {
await openSettings(page);
await page.getByRole("button", { name: /people/i }).click();
await page.waitForTimeout(500);
await page.getByRole("button", { name: /add members/i }).click();
await page.getByPlaceholder("Username").fill("shouldnotexist");
// Cancel
await page.getByRole("button", { name: /cancel/i }).click();
// Form should be hidden
await expect(page.getByPlaceholder("Username")).not.toBeVisible();
// Re-open -- the username field should be empty
await page.getByRole("button", { name: /add members/i }).click();
const usernameValue = await page.getByPlaceholder("Username").inputValue();
expect(usernameValue).toBe("");
await page.getByRole("button", { name: /cancel/i }).click();
});
test("user table row has avatar with first letter initial", async ({ loggedInPage: page }) => {
await openSettings(page);
await page.getByRole("button", { name: /people/i }).click();
await page.waitForTimeout(500);
// The admin row should have an avatar with "A" (first letter of admin)
const avatar = page.locator(".w-8.h-8.rounded-full").first();
await expect(avatar).toBeVisible();
await expect(avatar).toContainText("A");
});
test("people tab heading says 'People'", async ({ loggedInPage: page }) => {
await openSettings(page);
await page.getByRole("button", { name: /people/i }).click();
await expect(page.locator("h3").filter({ hasText: "People" })).toBeVisible();
});
});
// ===========================================================================
// TEAMS TAB -- expanded team management tests
// ===========================================================================
test.describe("Settings Teams Tab - Extended", () => {
test("teams tab heading and description", async ({ loggedInPage: page }) => {
await openSettings(page);
await page.getByRole("button", { name: /teams/i }).click();
await expect(page.locator("h3").filter({ hasText: "Teams" })).toBeVisible();
await expect(page.getByText(/manage.*team/i).first()).toBeVisible();
});
test("Create New Team button toggles the form", async ({ loggedInPage: page }) => {
await openSettings(page);
await page.getByRole("button", { name: /teams/i }).click();
// Click to open form
await page.getByRole("button", { name: /create new team/i }).click();
await expect(page.getByPlaceholder("Team name")).toBeVisible();
// Click again to close form
await page.getByRole("button", { name: /create new team/i }).click();
await expect(page.getByPlaceholder("Team name")).not.toBeVisible();
});
test("team three-dot menu shows Rename and Delete options", async ({ loggedInPage: page }) => {
await openSettings(page);
await page.getByRole("button", { name: /teams/i }).click();
await page.waitForTimeout(500);
// Open the three-dot menu for the first team
const moreButtons = page.locator("button:has(svg.lucide-ellipsis-vertical)");
await moreButtons.first().click();
await expect(page.locator("[role='menu']").getByText("Rename")).toBeVisible();
await expect(page.locator("[role='menu']").getByText("Delete")).toBeVisible();
});
test("team rename via Enter key works", async ({ loggedInPage: page }) => {
const teamName = `enterteam-${UID}`;
const renamedName = `enterrenamed-${UID}`;
const adminToken = await getAdminToken();
try {
// Create a team via API
await fetch(`${API}/api/v1/teams`, {
method: "POST",
headers: authJson(adminToken),
body: JSON.stringify({ name: teamName }),
});
await openSettings(page);
await page.getByRole("button", { name: /teams/i }).click();
await page.waitForTimeout(500);
await expect(page.getByText(teamName)).toBeVisible({ timeout: 5_000 });
// Open menu and rename
const moreButtons = page.locator("button:has(svg.lucide-ellipsis-vertical)");
await moreButtons.last().click();
await page.locator("[role='menu']").getByText("Rename").click();
// Inline edit input should appear
const renameInput = page.locator(
"input.px-2.py-1.rounded.border.border-border.bg-background",
);
await expect(renameInput).toBeVisible({ timeout: 3_000 });
await renameInput.fill(renamedName);
await renameInput.press("Enter");
// Updated name should appear
await expect(page.getByText(renamedName)).toBeVisible({ timeout: 5_000 });
} finally {
await cleanupTeamsByPrefix(adminToken, "enterteam-");
await cleanupTeamsByPrefix(adminToken, "enterrenamed-");
}
});
test("team rename cancel via Escape key works", async ({ loggedInPage: page }) => {
await openSettings(page);
await page.getByRole("button", { name: /teams/i }).click();
await page.waitForTimeout(500);
// Open menu for Default team and click Rename
const moreButtons = page.locator("button:has(svg.lucide-ellipsis-vertical)");
await moreButtons.first().click();
await page.locator("[role='menu']").getByText("Rename").click();
// Inline edit input should appear
const renameInput = page.locator("input.px-2.py-1.rounded.border.border-border.bg-background");
await expect(renameInput).toBeVisible({ timeout: 3_000 });
// Press Escape to cancel
await renameInput.press("Escape");
// Input should disappear
await expect(renameInput).not.toBeVisible();
// Default team name should still be visible
await expect(page.getByText("Default").first()).toBeVisible();
});
test("creating multiple teams shows correct member counts", async ({ loggedInPage: page }) => {
const teamName1 = `multi1-${UID}`;
const teamName2 = `multi2-${UID}`;
const adminToken = await getAdminToken();
try {
await fetch(`${API}/api/v1/teams`, {
method: "POST",
headers: authJson(adminToken),
body: JSON.stringify({ name: teamName1 }),
});
await fetch(`${API}/api/v1/teams`, {
method: "POST",
headers: authJson(adminToken),
body: JSON.stringify({ name: teamName2 }),
});
await openSettings(page);
await page.getByRole("button", { name: /teams/i }).click();
await page.waitForTimeout(500);
await expect(page.getByText(teamName1)).toBeVisible({ timeout: 5_000 });
await expect(page.getByText(teamName2)).toBeVisible({ timeout: 5_000 });
} finally {
await cleanupTeamsByPrefix(adminToken, "multi1-");
await cleanupTeamsByPrefix(adminToken, "multi2-");
}
});
});
// ===========================================================================
// ROLES TAB -- expanded custom role tests
// ===========================================================================
test.describe("Settings Roles Tab - Extended custom role management", () => {
test("creating a role without a name is prevented", async ({ loggedInPage: page }) => {
await openSettings(page);
await page.getByRole("button", { name: /^roles$/i }).click();
await page.getByRole("button", { name: /create custom role/i }).click();
// Leave name empty and try to submit
const nameInput = page.getByPlaceholder("Role name");
await expect(nameInput).toHaveAttribute("required", "");
// Cancel
await page.getByRole("button", { name: /cancel/i }).click();
});
test("creating a custom role with duplicate name shows error", async ({ loggedInPage: page }) => {
const roleName = `exdup${UID}`;
const adminToken = await getAdminToken();
try {
// Create role via API first
await fetch(`${API}/api/v1/roles`, {
method: "POST",
headers: authJson(adminToken),
body: JSON.stringify({
name: roleName,
description: "Dup test",
permissions: ["tools:use"],
}),
});
await openSettings(page);
await page.getByRole("button", { name: /^roles$/i }).click();
await expect(page.getByText(roleName)).toBeVisible({ timeout: 5_000 });
// Try to create same role via GUI
await page.getByRole("button", { name: /create custom role/i }).click();
await page.getByPlaceholder("Role name").fill(roleName);
await page.getByRole("button", { name: /^create$/i }).click();
// Should show duplicate error
await expect(page.getByText(/already exists|duplicate/i).first()).toBeVisible({
timeout: 5_000,
});
} finally {
await cleanupRolesByPrefix(adminToken, roleName);
}
});
test("admin built-in role shows correct permissions", async ({ loggedInPage: page }) => {
await openSettings(page);
await page.getByRole("button", { name: /^roles$/i }).click();
// Admin role should have all permissions displayed
// Look for key permissions that admin role must have
await expect(
page.locator(".font-mono").filter({ hasText: "users:manage" }).first(),
).toBeVisible();
await expect(
page.locator(".font-mono").filter({ hasText: "settings:write" }).first(),
).toBeVisible();
});
test("Cancel button on edit role form closes it", async ({ loggedInPage: page }) => {
const roleName = `excancel${UID}`;
const adminToken = await getAdminToken();
try {
await fetch(`${API}/api/v1/roles`, {
method: "POST",
headers: authJson(adminToken),
body: JSON.stringify({
name: roleName,
description: "Cancel test",
permissions: ["tools:use"],
}),
});
await openSettings(page);
await page.getByRole("button", { name: /^roles$/i }).click();
await expect(page.getByText(roleName)).toBeVisible({ timeout: 5_000 });
// Open edit
await page.locator("button[title='Edit role']").first().click();
await expect(page.getByText(/edit role/i)).toBeVisible();
// Cancel
await page.getByRole("button", { name: /cancel/i }).click();
// Edit form should be hidden
await expect(page.getByText(/edit role/i)).not.toBeVisible();
} finally {
await cleanupRolesByPrefix(adminToken, roleName);
}
});
test("editing a custom role description updates it", async ({ loggedInPage: page }) => {
const roleName = `exdesc${UID}`;
const adminToken = await getAdminToken();
try {
await fetch(`${API}/api/v1/roles`, {
method: "POST",
headers: authJson(adminToken),
body: JSON.stringify({
name: roleName,
description: "Original desc",
permissions: ["tools:use"],
}),
});
await openSettings(page);
await page.getByRole("button", { name: /^roles$/i }).click();
await expect(page.getByText(roleName)).toBeVisible({ timeout: 5_000 });
// Edit the role
await page.locator("button[title='Edit role']").first().click();
await expect(page.getByText(/edit role/i)).toBeVisible();
// Change description
const descInput = page.getByPlaceholder("Description (optional)");
await descInput.fill("Updated desc");
// Save
await page.getByRole("button", { name: /^save$/i }).click();
await expect(page.getByText("Role updated")).toBeVisible({ timeout: 5_000 });
// Updated description should appear on the card
await expect(page.getByText("Updated desc")).toBeVisible();
} finally {
await cleanupRolesByPrefix(adminToken, roleName);
}
});
});
// ===========================================================================
// AUDIT LOG TAB -- expanded tests
// ===========================================================================
test.describe("Settings Audit Log Tab - Extended", () => {
test("audit log pagination controls appear when entries exceed page limit", async ({
loggedInPage: page,
}) => {
await openSettings(page);
await page.getByRole("button", { name: /audit log/i }).click();
// Wait for table to load
await expect(page.locator("h3").filter({ hasText: "Audit Log" })).toBeVisible();
// Pagination controls may or may not appear depending on entry count
// Just verify the component loads without error
await page.waitForTimeout(1_000);
});
test("filter dropdown includes USER_UPDATED and PASSWORD_RESET actions", async ({
loggedInPage: page,
}) => {
await openSettings(page);
await page.getByRole("button", { name: /audit log/i }).click();
const filterSelect = page.locator("select").first();
await expect(filterSelect).toBeVisible();
for (const action of ["USER_UPDATED", "PASSWORD_RESET", "API_KEY_DELETED", "ROLE_CREATED"]) {
await expect(filterSelect.locator(`option[value='${action}']`)).toBeAttached();
}
});
test("USER_CREATED entry appears after creating a user", async ({ loggedInPage: page }) => {
const username = `auditcreate-${UID}`;
const adminToken = await getAdminToken();
try {
// Create a user via API to generate a USER_CREATED audit entry
await fetch(`${API}/api/auth/register`, {
method: "POST",
headers: authJson(adminToken),
body: JSON.stringify({ username, password: "AuditPass123!", role: "user" }),
});
await openSettings(page);
await page.getByRole("button", { name: /audit log/i }).click();
await expect(page.locator("table thead")).toBeVisible({ timeout: 10_000 });
const filterSelect = page.locator("select").first();
await filterSelect.selectOption("USER_CREATED");
await expect(page.locator("table tbody tr").first()).toBeVisible({ timeout: 10_000 });
const tableText = await page.locator("table tbody").textContent();
expect(tableText).toContain("USER_CREATED");
} finally {
await cleanupUsersByPrefix(adminToken, "auditcreate-");
}
});
test("API_KEY_CREATED entry appears after generating an API key", async ({
loggedInPage: page,
}) => {
await openSettings(page);
// Generate an API key to trigger the audit entry
await page.getByRole("button", { name: /api keys/i }).click();
const keyName = `auditkey-${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 });
// Clean up the key
page.on("dialog", (d) => d.accept());
await page.locator("button[title='Delete key']").first().click();
await page.waitForTimeout(500);
// Check audit log for API_KEY_CREATED
await page.getByRole("button", { name: /audit log/i }).click();
await expect(page.locator("table thead")).toBeVisible({ timeout: 10_000 });
const filterSelect = page.locator("select").first();
await filterSelect.selectOption("API_KEY_CREATED");
await expect(page.locator("table tbody tr").first()).toBeVisible({ timeout: 10_000 });
const tableText = await page.locator("table tbody").textContent();
expect(tableText).toContain("API_KEY_CREATED");
});
});
// ===========================================================================
// API KEYS TAB -- expanded tests
// ===========================================================================
test.describe("Settings API Keys Tab - Extended", () => {
test("API keys tab shows heading and description", async ({ loggedInPage: page }) => {
await openSettings(page);
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("generating key with expiration date shows expiration in the list", async ({
loggedInPage: page,
}) => {
await openSettings(page);
await page.getByRole("button", { name: /api keys/i }).click();
const keyName = `expTest-${Date.now()}`;
await page.getByPlaceholder("Key name (optional)").fill(keyName);
// Set an expiration date
const dateInput = page.locator("input[type='datetime-local']");
await dateInput.fill("2030-12-31T23:59");
// Generate
await page.getByRole("button", { name: /generate api key/i }).click();
await expect(page.locator("code.font-mono")).toBeVisible({ timeout: 5_000 });
// The key entry should show an expiration indicator
await expect(page.getByText(/Expires/).first()).toBeVisible();
// Clean up
page.on("dialog", (d) => d.accept());
await page.locator("button[title='Delete key']").first().click();
await page.waitForTimeout(500);
});
test("multiple API keys can be created and listed", async ({ loggedInPage: page }) => {
await openSettings(page);
await page.getByRole("button", { name: /api keys/i }).click();
const key1Name = `multi1-${Date.now()}`;
const key2Name = `multi2-${Date.now()}`;
// Create first key
await page.getByPlaceholder("Key name (optional)").fill(key1Name);
await page.getByRole("button", { name: /generate api key/i }).click();
await expect(page.locator("code.font-mono")).toBeVisible({ timeout: 5_000 });
// Create second key
await page.getByPlaceholder("Key name (optional)").fill(key2Name);
await page.getByRole("button", { name: /generate api key/i }).click();
await page.waitForTimeout(1_000);
// Both should appear in the Existing Keys list
await expect(page.getByText(key1Name)).toBeVisible();
await expect(page.getByText(key2Name)).toBeVisible();
// Clean up both
page.on("dialog", (d) => d.accept());
const deleteButtons = page.locator("button[title='Delete key']");
const count = await deleteButtons.count();
for (let i = count - 1; i >= 0; i--) {
const btn = deleteButtons.nth(i);
const isVis = await btn.isVisible().catch(() => false);
if (isVis) {
await btn.click();
await page.waitForTimeout(500);
}
}
});
test("scoped permission checkbox labels include permission names", async ({
loggedInPage: page,
}) => {
await openSettings(page);
await page.getByRole("button", { name: /api keys/i }).click();
await page.getByText("Restrict permissions (optional)").click();
await expect(page.getByText("Remove permission scoping")).toBeVisible();
// Key permissions should be listed as checkbox labels
for (const perm of ["tools:use", "files:own"]) {
await expect(page.getByText(perm).first()).toBeVisible();
}
// Collapse
await page.getByText("Remove permission scoping").click();
await expect(page.getByText("Restrict permissions (optional)")).toBeVisible();
});
});
// ===========================================================================
// AI FEATURES TAB -- expanded tests
// ===========================================================================
test.describe("Settings AI Features Tab - Extended", () => {
test("AI Features tab shows correct heading", async ({ loggedInPage: page }) => {
await openSettings(page);
await page.getByRole("button", { name: /ai features/i }).click();
await expect(page.locator("h3").filter({ hasText: "AI Features" })).toBeVisible();
});
test("AI Features tab shows Install All button", async ({ loggedInPage: page }) => {
await openSettings(page);
await page.getByRole("button", { name: /ai features/i }).click();
await expect(page.getByRole("button", { name: /install all/i })).toBeVisible();
});
test("each AI bundle card shows a name", async ({ loggedInPage: page }) => {
await openSettings(page);
await page.getByRole("button", { name: /ai features/i }).click();
// Wait for bundles to load
const bundleCards = page.locator(".rounded-lg.border.border-border.p-4");
await expect(bundleCards.first()).toBeVisible({ timeout: 5_000 });
// First bundle card should have text content (bundle name)
const firstCardText = await bundleCards.first().textContent();
expect(firstCardText?.length).toBeGreaterThan(0);
});
});
// ===========================================================================
// TOOLS TAB -- expanded tests
// ===========================================================================
test.describe("Settings Tools Tab - Extended", () => {
test("tools section has a search input", async ({ loggedInPage: page }) => {
await openSettings(page);
await page.getByRole("button", { name: /tools/i }).click();
await expect(page.getByText(/\d+ tools? disabled/)).toBeVisible({ timeout: 5_000 });
const dialogContent = page.locator(".flex-1.overflow-y-auto");
const searchInput = dialogContent.getByPlaceholder("Search tools...");
await expect(searchInput).toBeVisible();
});
test("search for a specific tool shows only matching results", async ({ loggedInPage: page }) => {
await openSettings(page);
await page.getByRole("button", { name: /tools/i }).click();
await expect(page.getByText(/\d+ tools? disabled/)).toBeVisible({ timeout: 5_000 });
const dialogContent = page.locator(".flex-1.overflow-y-auto");
const searchInput = dialogContent.getByPlaceholder("Search tools...");
// Search for "Compress"
await searchInput.fill("Compress");
// Compress tool should be visible
await expect(dialogContent.getByText("Compress").first()).toBeVisible();
// Clear search
await searchInput.fill("");
});
test("saving tool settings shows restart banner", async ({ loggedInPage: page }) => {
await openSettings(page);
await page.getByRole("button", { name: /tools/i }).click();
await expect(page.getByText(/\d+ tools? disabled/)).toBeVisible({ timeout: 5_000 });
await page.getByRole("button", { name: /save tool settings/i }).click();
await expect(page.getByText("Restart required for changes to take effect.")).toBeVisible({
timeout: 5_000,
});
});
test("tool descriptions are visible in the tool list", async ({ loggedInPage: page }) => {
await openSettings(page);
await page.getByRole("button", { name: /tools/i }).click();
await expect(page.getByText(/\d+ tools? disabled/)).toBeVisible({ timeout: 5_000 });
// Each tool row has a description in text-xs text-muted-foreground
const descriptions = page.locator(".flex-1.overflow-y-auto .text-xs.text-muted-foreground");
const count = await descriptions.count();
expect(count).toBeGreaterThan(5);
});
});
// ===========================================================================
// RBAC VERIFICATION -- expanded cross-role API endpoint checks
// ===========================================================================
base.describe("RBAC API - Editor expanded endpoint checks", () => {
const EDITOR_EXP = `rbaceditorexp-${UID}`;
const EDITOR_EXP_PASS = "EditorExpPass1";
let adminToken: string;
base.beforeAll(async () => {
adminToken = await getAdminToken();
await createReadyUser(adminToken, EDITOR_EXP, EDITOR_EXP_PASS, "editor");
});
base.afterAll(async () => {
await deleteUser(adminToken, EDITOR_EXP);
});
base.test("editor gets 403 on DELETE user endpoint", async ({ page }) => {
await login(page, EDITOR_EXP, EDITOR_EXP_PASS);
const token = await page.evaluate(() => localStorage.getItem("snapotter-token"));
expect(token).toBeTruthy();
// Try to delete a user (even a fake ID)
const res = await fetch(`${API}/api/auth/users/99999`, {
method: "DELETE",
headers: { Authorization: `Bearer ${token}` },
});
expect(res.status).toBe(403);
});
base.test("editor gets 403 on PUT settings endpoint", async ({ page }) => {
await login(page, EDITOR_EXP, EDITOR_EXP_PASS);
const token = await page.evaluate(() => localStorage.getItem("snapotter-token"));
expect(token).toBeTruthy();
const res = await fetch(`${API}/api/v1/settings`, {
method: "PUT",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ fileUploadLimitMb: "999" }),
});
expect(res.status).toBe(403);
});
base.test("editor gets 403 on POST roles endpoint", async ({ page }) => {
await login(page, EDITOR_EXP, EDITOR_EXP_PASS);
const token = await page.evaluate(() => localStorage.getItem("snapotter-token"));
expect(token).toBeTruthy();
const res = await fetch(`${API}/api/v1/roles`, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ name: "hacked-role", permissions: ["users:manage"] }),
});
expect(res.status).toBe(403);
});
base.test("editor can access own session endpoint", async ({ page }) => {
await login(page, EDITOR_EXP, EDITOR_EXP_PASS);
const token = await page.evaluate(() => localStorage.getItem("snapotter-token"));
expect(token).toBeTruthy();
const res = await fetch(`${API}/api/auth/session`, {
headers: { Authorization: `Bearer ${token}` },
});
expect(res.status).toBe(200);
const data = await res.json();
expect(data.user.username).toBe(EDITOR_EXP);
expect(data.user.role).toBe("editor");
});
});
base.describe("RBAC API - User expanded endpoint checks", () => {
const USER_EXP = `rbacuserexp-${UID}`;
const USER_EXP_PASS = "UserExpPass1";
let adminToken: string;
base.beforeAll(async () => {
adminToken = await getAdminToken();
await createReadyUser(adminToken, USER_EXP, USER_EXP_PASS, "user");
});
base.afterAll(async () => {
await deleteUser(adminToken, USER_EXP);
});
base.test("user gets 403 on DELETE user endpoint", async ({ page }) => {
await login(page, USER_EXP, USER_EXP_PASS);
const token = await page.evaluate(() => localStorage.getItem("snapotter-token"));
expect(token).toBeTruthy();
const res = await fetch(`${API}/api/auth/users/99999`, {
method: "DELETE",
headers: { Authorization: `Bearer ${token}` },
});
expect(res.status).toBe(403);
});
base.test("user gets 403 on POST teams endpoint", async ({ page }) => {
await login(page, USER_EXP, USER_EXP_PASS);
const token = await page.evaluate(() => localStorage.getItem("snapotter-token"));
expect(token).toBeTruthy();
const res = await fetch(`${API}/api/v1/teams`, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ name: "hacked-team" }),
});
expect(res.status).toBe(403);
});
base.test("user gets 403 on POST roles endpoint", async ({ page }) => {
await login(page, USER_EXP, USER_EXP_PASS);
const token = await page.evaluate(() => localStorage.getItem("snapotter-token"));
expect(token).toBeTruthy();
const res = await fetch(`${API}/api/v1/roles`, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ name: "hacked-role", permissions: ["users:manage"] }),
});
expect(res.status).toBe(403);
});
base.test("user gets 403 on PUT settings endpoint", async ({ page }) => {
await login(page, USER_EXP, USER_EXP_PASS);
const token = await page.evaluate(() => localStorage.getItem("snapotter-token"));
expect(token).toBeTruthy();
const res = await fetch(`${API}/api/v1/settings`, {
method: "PUT",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ fileUploadLimitMb: "999" }),
});
expect(res.status).toBe(403);
});
base.test("user can change own password via API", async ({ page }) => {
await login(page, USER_EXP, USER_EXP_PASS);
const token = await page.evaluate(() => localStorage.getItem("snapotter-token"));
expect(token).toBeTruthy();
// Change password to same value
const res = await fetch(`${API}/api/auth/change-password`, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
currentPassword: USER_EXP_PASS,
newPassword: USER_EXP_PASS,
}),
});
expect(res.status).toBe(200);
});
base.test("user can generate own API key", async ({ page }) => {
await login(page, USER_EXP, USER_EXP_PASS);
const token = await page.evaluate(() => localStorage.getItem("snapotter-token"));
expect(token).toBeTruthy();
const res = await fetch(`${API}/api/v1/api-keys`, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ name: "user-test-key" }),
});
expect(res.status).toBe(201);
// Clean up: list and delete the key
const listRes = await fetch(`${API}/api/v1/api-keys`, {
headers: { Authorization: `Bearer ${token}` },
});
const listData = await listRes.json();
for (const k of listData.apiKeys) {
if (k.name === "user-test-key") {
await fetch(`${API}/api/v1/api-keys/${k.id}`, {
method: "DELETE",
headers: { Authorization: `Bearer ${token}` },
});
}
}
});
});
// ===========================================================================
// RBAC -- Admin GUI-level tests for admin-only tabs
// ===========================================================================
base.describe("RBAC GUI - Admin admin-only tab deep access", () => {
base.use({ storageState: ".playwright/.auth/user.json" });
base.test("admin can open Roles tab and see Create Custom Role button", async ({ page }) => {
await page.goto("/");
await openSettings(page);
await page.getByRole("button", { name: /^roles$/i }).click();
await expect(page.locator("h3").filter({ hasText: "Roles" })).toBeVisible();
await expect(page.getByRole("button", { name: /create custom role/i })).toBeVisible();
});
base.test("admin can open Audit Log and filter by specific action", async ({ page }) => {
await page.goto("/");
await openSettings(page);
await page.getByRole("button", { name: /audit log/i }).click();
await expect(page.locator("h3").filter({ hasText: "Audit Log" })).toBeVisible();
const filterSelect = page.locator("select").first();
await filterSelect.selectOption("LOGIN_SUCCESS");
// Wait for filtered results
await page.waitForTimeout(1_000);
});
base.test("admin can save system settings without error", async ({ page }) => {
await page.goto("/");
await openSettings(page);
await page.getByRole("button", { name: /system settings/i }).click();
await expect(page.getByText("File Upload Limit (MB)")).toBeVisible();
await page.getByRole("button", { name: /save settings/i }).click();
await expect(page.getByText("Settings saved.")).toBeVisible({ timeout: 5_000 });
});
});
// ===========================================================================
// RBAC -- Editor and User GUI-level tab content verification
// ===========================================================================
base.describe("RBAC GUI - Editor tab content access", () => {
const EDITOR_GUI = `rbacedgui-${UID}`;
const EDITOR_GUI_PASS = "EdGuiPass1";
let adminToken: string;
base.beforeAll(async () => {
adminToken = await getAdminToken();
await createReadyUser(adminToken, EDITOR_GUI, EDITOR_GUI_PASS, "editor");
});
base.afterAll(async () => {
await deleteUser(adminToken, EDITOR_GUI);
});
base.test("editor can change password from Security tab", async ({ page }) => {
await login(page, EDITOR_GUI, EDITOR_GUI_PASS);
await openSettings(page);
await page.getByRole("button", { name: /security/i }).click();
await page.getByPlaceholder("Current Password").fill(EDITOR_GUI_PASS);
await page.getByPlaceholder("New Password").first().fill(EDITOR_GUI_PASS);
await page.getByPlaceholder("Confirm New Password").fill(EDITOR_GUI_PASS);
await page.getByRole("button", { name: /change password/i }).click();
await expect(page.getByText("Password changed successfully")).toBeVisible({ timeout: 5_000 });
});
base.test("editor can generate and delete API key from GUI", async ({ page }) => {
await login(page, EDITOR_GUI, EDITOR_GUI_PASS);
await openSettings(page);
await page.getByRole("button", { name: /api keys/i }).click();
const keyName = `editorKey-${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 });
await expect(page.getByText(keyName)).toBeVisible();
// Delete the key
page.on("dialog", (d) => d.accept());
await page.locator("button[title='Delete key']").first().click();
await expect(page.getByText(keyName)).not.toBeVisible({ timeout: 5_000 });
});
base.test("editor can view About tab with version info", async ({ page }) => {
await login(page, EDITOR_GUI, EDITOR_GUI_PASS);
await openSettings(page);
await page.getByRole("button", { name: /about/i }).click();
await expect(page.locator("h3").filter({ hasText: "About" })).toBeVisible();
await expect(page.getByText("Version:")).toBeVisible();
await expect(page.getByText("SnapOtter").first()).toBeVisible();
});
});
base.describe("RBAC GUI - User tab content access", () => {
const USER_GUI = `rbacusrgui-${UID}`;
const USER_GUI_PASS = "UsrGuiPass1";
let adminToken: string;
base.beforeAll(async () => {
adminToken = await getAdminToken();
await createReadyUser(adminToken, USER_GUI, USER_GUI_PASS, "user");
});
base.afterAll(async () => {
await deleteUser(adminToken, USER_GUI);
});
base.test("user can save General tab preferences", async ({ page }) => {
await login(page, USER_GUI, USER_GUI_PASS);
await openSettings(page);
await expect(page.getByText("Default Tool View")).toBeVisible();
await page.getByRole("button", { name: /save settings/i }).click();
await expect(page.getByText("Settings saved.")).toBeVisible({ timeout: 5_000 });
});
base.test("user can change own password from Security tab", async ({ page }) => {
await login(page, USER_GUI, USER_GUI_PASS);
await openSettings(page);
await page.getByRole("button", { name: /security/i }).click();
await page.getByPlaceholder("Current Password").fill(USER_GUI_PASS);
await page.getByPlaceholder("New Password").first().fill(USER_GUI_PASS);
await page.getByPlaceholder("Confirm New Password").fill(USER_GUI_PASS);
await page.getByRole("button", { name: /change password/i }).click();
await expect(page.getByText("Password changed successfully")).toBeVisible({ timeout: 5_000 });
});
base.test("user can toggle tool visibility in Tools tab", async ({ page }) => {
await login(page, USER_GUI, USER_GUI_PASS);
await openSettings(page);
await page.getByRole("button", { name: /tools/i }).click();
await expect(page.getByText(/\d+ tools? disabled/)).toBeVisible({ timeout: 5_000 });
const counterText = page.getByText(/\d+ tools? disabled/);
const initialText = await counterText.textContent();
const initialCount = parseInt(initialText?.match(/(\d+)/)?.[1] || "0", 10);
// Toggle the first tool
const firstToggle = page.locator("button.w-11.h-6").first();
await firstToggle.click();
const updatedText = await counterText.textContent();
const updatedCount = parseInt(updatedText?.match(/(\d+)/)?.[1] || "0", 10);
expect(Math.abs(updatedCount - initialCount)).toBe(1);
// Revert
await firstToggle.click();
});
base.test("user can generate API key from GUI", async ({ page }) => {
await login(page, USER_GUI, USER_GUI_PASS);
await openSettings(page);
await page.getByRole("button", { name: /api keys/i }).click();
const keyName = `userKey-${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 });
await expect(page.getByText("Store this key securely")).toBeVisible();
// Clean up
page.on("dialog", (d) => d.accept());
await page.locator("button[title='Delete key']").first().click();
await page.waitForTimeout(500);
});
});