Files
SnapOtter/tests/e2e/gui-settings-people.spec.ts
T
SnapOtter a0556772e8 test: expand coverage across all layers -- 1,268 new tests, fix replace-color div-by-zero
14-agent parallel test expansion covering integration, unit, E2E, E2E-Docker,
cross-format matrix, adversarial, GUI navigation/tools/settings/visual/a11y/perf.

- Integration: expand 23 tool test files with HEIC, stress, batch, edge cases
- Unit: close coverage gaps in image-engine, stores, lib (metadata, auto-enhance,
  connection-store, lazy-with-retry, collage/file-store HEIC preview)
- AI bridge: 141 new tests for dispatcher buffering, crash recovery, OOM/segfault
- Cross-format: 794 parameterized tests (16 formats x 12 tools + no-crash matrix)
- Adversarial: memory stress (50x large file), zero-byte, corrupted headers, unicode
- E2E-Docker: expand 8 spec files with dimension verification, pipeline chains
- GUI E2E: tool UI settings/interactions for all 47 tools, remove all test.skip,
  RBAC per-role verification, visual screenshot naming, cross-browser smoke tests,
  a11y ARIA/focus/contrast, performance budgets, 15-tool stability test
- Fix: replace-color.ts tolerance=0 caused division-by-zero producing NaN pixels

Total: 8,958 tests passing across 202 files. Zero failures, zero skips.
2026-05-09 18:02:58 +08:00

722 lines
26 KiB
TypeScript

import { expect, 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),
});
}
}
}
// ---------------------------------------------------------------------------
// Settings Dialog -- People tab (user CRUD), Teams, and Roles
// ---------------------------------------------------------------------------
const UID = Date.now().toString(36);
test.describe("GUI Settings - People Tab", () => {
test("displays user count and user table", async ({ loggedInPage: page }) => {
await openSettings(page);
await page.getByRole("button", { name: /people/i }).click();
// User count
await expect(page.getByText(/\d+ users?/)).toBeVisible({ timeout: 5_000 });
// Table headers
await expect(page.getByText("User").first()).toBeVisible();
await expect(page.getByText("Role").first()).toBeVisible();
await expect(page.getByText("Team").first()).toBeVisible();
// Admin user row
await expect(page.getByText("admin").first()).toBeVisible();
});
test("search filters users and shows empty state", async ({ loggedInPage: page }) => {
await openSettings(page);
await page.getByRole("button", { name: /people/i }).click();
await page.waitForTimeout(500);
const searchInput = page.getByPlaceholder("Search members...");
await searchInput.fill("zzzznonexistent");
await expect(page.getByText("No members match your search.")).toBeVisible();
// Clear search restores admin
await searchInput.fill("");
await expect(page.getByText("admin").first()).toBeVisible();
});
test("Add Members opens form with username, password, role, team fields", async ({
loggedInPage: page,
}) => {
await openSettings(page);
await page.getByRole("button", { name: /people/i }).click();
await page.waitForTimeout(500);
const addBtn = page.getByRole("button", { name: /add members/i });
await expect(addBtn).toBeVisible();
const isDisabled = await addBtn.isDisabled();
if (!isDisabled) {
await addBtn.click();
await expect(page.getByPlaceholder("Username")).toBeVisible();
await expect(page.getByPlaceholder("Password")).toBeVisible();
// Role and team dropdowns (two <select> elements inside the form)
const selects = page.locator("form select");
await expect(selects.first()).toBeVisible();
await expect(selects.nth(1)).toBeVisible();
// Form action buttons
await expect(page.getByRole("button", { name: /create/i })).toBeVisible();
await expect(page.getByRole("button", { name: /cancel/i })).toBeVisible();
}
});
test("role dropdown in add form contains admin, editor, and user options", 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 role dropdown is the first select in the form
const roleSelect = page.locator("form select").first();
await expect(roleSelect).toBeVisible();
// Verify all three built-in role options are available
await expect(roleSelect.locator("option[value='admin']")).toBeAttached();
await expect(roleSelect.locator("option[value='editor']")).toBeAttached();
await expect(roleSelect.locator("option[value='user']")).toBeAttached();
// Cancel the form
await page.getByRole("button", { name: /cancel/i }).click();
});
test("creating a user with editor role shows correct role in table", async ({
loggedInPage: page,
}) => {
const username = `guieditor-${UID}`;
let adminToken: string;
try {
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 editor role
const roleSelect = page.locator("form select").first();
await roleSelect.selectOption("editor");
await page.getByRole("button", { name: /create/i }).click();
// User should appear in the table with editor role
await expect(page.getByText(username)).toBeVisible({ timeout: 5_000 });
await expect(page.getByText("EDITOR")).toBeVisible();
} finally {
adminToken = await getAdminToken();
await cleanupUsersByPrefix(adminToken, "guieditor-");
}
});
test("creating a user via the form adds them to the table", async ({ loggedInPage: page }) => {
const username = `guipeople-${UID}`;
let adminToken: string;
try {
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!");
await page.getByRole("button", { name: /create/i }).click();
// User should appear in the table
await expect(page.getByText(username)).toBeVisible({ timeout: 5_000 });
} finally {
// Clean up via API
adminToken = await getAdminToken();
await cleanupUsersByPrefix(adminToken, "guipeople-");
}
});
test("duplicate username shows error", async ({ loggedInPage: page }) => {
const username = `guidup-${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" }),
});
// Try to create same username via GUI
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!");
await page.getByRole("button", { name: /create/i }).click();
// Should show an error (409 conflict mapped to user-facing message)
await expect(page.locator(".text-destructive").first()).toBeVisible({ timeout: 5_000 });
} finally {
await cleanupUsersByPrefix(adminToken, "guidup-");
}
});
test("three-dot menu shows Edit Role/Team, Reset Password, and Delete", async ({
loggedInPage: page,
}) => {
await openSettings(page);
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 expect(page.getByText("Edit Role / Team")).toBeVisible();
await expect(page.getByText("Reset Password")).toBeVisible();
await expect(page.getByText("Delete User")).toBeVisible();
});
test("cannot delete yourself via the menu", async ({ loggedInPage: page }) => {
await openSettings(page);
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();
// Accept the confirm dialog
page.on("dialog", (d) => d.accept());
await page.getByText("Delete User").click();
// Should show an error message since admin cannot delete themselves
await expect(page.getByText(/failed to delete|cannot/i).first()).toBeVisible({
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 openSettings(page);
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 openSettings(page);
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 openSettings(page);
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("editing a user role via Edit Role / Team succeeds", async ({ loggedInPage: page }) => {
const username = `guiedit-${UID}`;
const adminToken = await getAdminToken();
try {
// Create a user via API first
await fetch(`${API}/api/auth/register`, {
method: "POST",
headers: authJson(adminToken),
body: JSON.stringify({ username, password: "TestPass123!", role: "user" }),
});
await openSettings(page);
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();
await page.getByText("Edit Role / Team").click();
// The edit form should appear
await expect(page.getByText(/edit/i).first()).toBeVisible();
// Change role to editor
const roleSelect = page.locator("form select").first();
await roleSelect.selectOption("editor");
// Click Save
await page.getByRole("button", { name: /^save$/i }).click();
// Wait for the update to complete and verify no error
await page.waitForTimeout(1_000);
// The user row should still be visible (edit was successful)
await expect(page.getByText(username)).toBeVisible();
} finally {
await cleanupUsersByPrefix(adminToken, "guiedit-");
}
});
test("resetting password for a non-admin user succeeds", async ({ loggedInPage: page }) => {
const username = `guireset-${UID}`;
const adminToken = await getAdminToken();
try {
// Create a user via API
await fetch(`${API}/api/auth/register`, {
method: "POST",
headers: authJson(adminToken),
body: JSON.stringify({ username, password: "TestPass123!", role: "user" }),
});
await openSettings(page);
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();
await page.getByText("Reset Password").click();
// Fill in the new password
await expect(page.getByText(/reset password for/i)).toBeVisible();
await page.getByPlaceholder(/new password/i).fill("NewResetPass123!");
// Submit
await page.getByRole("button", { name: /reset password/i }).click();
// Should show success or dismiss the form
await page.waitForTimeout(1_000);
// The reset form should close on success
await expect(page.getByText(/reset password for/i)).not.toBeVisible({ timeout: 5_000 });
} finally {
await cleanupUsersByPrefix(adminToken, "guireset-");
}
});
test("user table shows role in uppercase badge format", async ({ loggedInPage: page }) => {
await openSettings(page);
await page.getByRole("button", { name: /people/i }).click();
await page.waitForTimeout(500);
// Admin user should show ADMIN role badge
await expect(page.getByText("ADMIN").first()).toBeVisible();
});
});
test.describe("GUI Settings - Teams Tab", () => {
test("displays team list with Default team", async ({ loggedInPage: page }) => {
await openSettings(page);
await page.getByRole("button", { name: /teams/i }).click();
await expect(page.locator("h3").filter({ hasText: "Teams" })).toBeVisible();
// Default team should exist
await expect(page.getByText("Default").first()).toBeVisible();
// Table headers
await expect(page.getByText("Team Name")).toBeVisible();
await expect(page.getByText("Members").first()).toBeVisible();
});
test("Create New Team button opens form and creates a team", async ({ loggedInPage: page }) => {
const teamName = `guiteam-${UID}`;
const adminToken = await getAdminToken();
try {
await openSettings(page);
await page.getByRole("button", { name: /teams/i }).click();
await page.getByRole("button", { name: /create new team/i }).click();
await expect(page.getByPlaceholder("Team name")).toBeVisible();
await page.getByPlaceholder("Team name").fill(teamName);
await page.getByRole("button", { name: /^create$/i }).click();
// Team should appear in the list
await expect(page.getByText(teamName)).toBeVisible({ timeout: 5_000 });
} finally {
await cleanupTeamsByPrefix(adminToken, "guiteam-");
}
});
test("deleting a team removes it from the list", async ({ loggedInPage: page }) => {
const teamName = `guidelteam-${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: /teams/i }).click();
await page.waitForTimeout(500);
// Verify the team exists
await expect(page.getByText(teamName)).toBeVisible({ timeout: 5_000 });
// Open the three-dot menu for the test team row (last MoreVertical button)
const moreButtons = page.locator("button:has(svg.lucide-ellipsis-vertical)");
await moreButtons.last().click();
// Click Delete in the dropdown and accept the confirm dialog
page.on("dialog", (d) => d.accept());
await page.locator("[role='menu']").getByText("Delete").click();
// Team should no longer be visible
await expect(page.getByText(teamName)).not.toBeVisible({ timeout: 5_000 });
} finally {
await cleanupTeamsByPrefix(adminToken, "guidelteam-");
}
});
});
test.describe("GUI Settings - Roles Tab", () => {
test("displays built-in roles with Built-in badge", async ({ loggedInPage: page }) => {
await openSettings(page);
await page.getByRole("button", { name: /^roles$/i }).click();
await expect(page.locator("h3").filter({ hasText: "Roles" })).toBeVisible();
await expect(page.getByText("Manage roles and their permissions")).toBeVisible();
// Built-in badge should exist
await expect(page.getByText("Built-in").first()).toBeVisible();
// All three built-in roles should be present
await expect(page.getByText("admin").first()).toBeVisible();
await expect(page.getByText("editor").first()).toBeVisible();
await expect(page.getByText("user").first()).toBeVisible();
});
test("built-in roles do not show edit or delete buttons", async ({ loggedInPage: page }) => {
await openSettings(page);
await page.getByRole("button", { name: /^roles$/i }).click();
await expect(page.getByText("Built-in").first()).toBeVisible();
// Built-in role cards should not have edit/delete buttons
// The code only renders edit/delete for !role.isBuiltin
// Check that the first role card (a built-in one) has no "Edit role" or "Delete role" buttons
const builtinCard = page
.locator("div")
.filter({ has: page.getByText("Built-in") })
.first();
await expect(builtinCard.locator("button[title='Edit role']")).not.toBeVisible();
await expect(builtinCard.locator("button[title='Delete role']")).not.toBeVisible();
});
test("Create Custom Role button is visible", async ({ loggedInPage: page }) => {
await openSettings(page);
await page.getByRole("button", { name: /^roles$/i }).click();
await expect(page.getByRole("button", { name: /create custom role/i })).toBeVisible();
});
test("Create Custom Role opens form with name, description, and permissions", async ({
loggedInPage: page,
}) => {
await openSettings(page);
await page.getByRole("button", { name: /^roles$/i }).click();
await page.getByRole("button", { name: /create custom role/i }).click();
// Form fields should be visible
await expect(page.getByText("New Role")).toBeVisible();
await expect(page.getByPlaceholder("Role name")).toBeVisible();
await expect(page.getByPlaceholder("Description (optional)")).toBeVisible();
await expect(page.getByText("Permissions")).toBeVisible();
// Permission checkboxes should be present
await expect(page.locator("input[type='checkbox']").first()).toBeVisible();
await expect(page.getByText("tools:use")).toBeVisible();
// Create and Cancel buttons
await expect(page.getByRole("button", { name: /^create$/i })).toBeVisible();
await expect(page.getByRole("button", { name: /cancel/i })).toBeVisible();
// Cancel closes the form
await page.getByRole("button", { name: /cancel/i }).click();
await expect(page.getByText("New Role")).not.toBeVisible();
});
test("creating a custom role adds it to the list with edit/delete buttons", async ({
loggedInPage: page,
}) => {
const roleName = `guirole${UID}`;
const adminToken = await getAdminToken();
try {
await openSettings(page);
await page.getByRole("button", { name: /^roles$/i }).click();
await page.getByRole("button", { name: /create custom role/i }).click();
await page.getByPlaceholder("Role name").fill(roleName);
await page.getByPlaceholder("Description (optional)").fill("Test custom role");
// Select a permission
const toolsCheckbox = page
.locator("label")
.filter({ hasText: "tools:use" })
.locator("input[type='checkbox']");
await toolsCheckbox.check();
await page.getByRole("button", { name: /^create$/i }).click();
// Role should appear in the list
await expect(page.getByText(roleName)).toBeVisible({ timeout: 5_000 });
await expect(page.getByText("Role created successfully")).toBeVisible({ timeout: 3_000 });
// Custom roles have edit and delete buttons
await expect(page.locator("button[title='Edit role']").first()).toBeVisible();
await expect(page.locator("button[title='Delete role']").first()).toBeVisible();
} finally {
// Clean up via API
await fetch(`${API}/api/v1/roles`, { headers: authOnly(adminToken) })
.then((r) => r.json())
.then(async ({ roles }: { roles: Array<{ id: string; name: string }> }) => {
for (const r of roles) {
if (r.name === roleName) {
await fetch(`${API}/api/v1/roles/${r.id}`, {
method: "DELETE",
headers: authOnly(adminToken),
});
}
}
})
.catch(() => {});
}
});
test("editing a custom role updates its name", async ({ loggedInPage: page }) => {
const roleName = `guiedit${UID}`;
const updatedName = `guieditup${UID}`;
const adminToken = await getAdminToken();
try {
// Create role via API
await fetch(`${API}/api/v1/roles`, {
method: "POST",
headers: authJson(adminToken),
body: JSON.stringify({
name: roleName,
description: "Editable role",
permissions: ["tools:use"],
}),
});
await openSettings(page);
await page.getByRole("button", { name: /^roles$/i }).click();
await expect(page.getByText(roleName)).toBeVisible({ timeout: 5_000 });
// Click the edit button for the custom role
await page.locator("button[title='Edit role']").first().click();
// The edit form should appear
await expect(page.getByText(/edit role/i)).toBeVisible();
// Change the role name
const nameInput = page.getByPlaceholder("Role name");
await nameInput.fill(updatedName);
// Save
await page.getByRole("button", { name: /^save$/i }).click();
await expect(page.getByText("Role updated")).toBeVisible({ timeout: 5_000 });
// Updated name should appear
await expect(page.getByText(updatedName)).toBeVisible({ timeout: 5_000 });
} finally {
// Clean up both possible names
await fetch(`${API}/api/v1/roles`, { headers: authOnly(adminToken) })
.then((r) => r.json())
.then(async ({ roles }: { roles: Array<{ id: string; name: string }> }) => {
for (const r of roles) {
if (r.name === roleName || r.name === updatedName) {
await fetch(`${API}/api/v1/roles/${r.id}`, {
method: "DELETE",
headers: authOnly(adminToken),
});
}
}
})
.catch(() => {});
}
});
test("deleting a custom role removes it from the list", async ({ loggedInPage: page }) => {
const roleName = `guidel${UID}`;
const adminToken = await getAdminToken();
try {
// Create role via API
await fetch(`${API}/api/v1/roles`, {
method: "POST",
headers: authJson(adminToken),
body: JSON.stringify({
name: roleName,
description: "Deletable role",
permissions: ["tools:use"],
}),
});
await openSettings(page);
await page.getByRole("button", { name: /^roles$/i }).click();
await expect(page.getByText(roleName)).toBeVisible({ timeout: 5_000 });
// Click the delete button for the custom role
page.on("dialog", (d) => d.accept());
await page.locator("button[title='Delete role']").first().click();
// Role should be removed
await expect(page.getByText(roleName)).not.toBeVisible({ timeout: 5_000 });
await expect(page.getByText(/deleted/i)).toBeVisible({ timeout: 3_000 });
} finally {
// Cleanup fallback
await fetch(`${API}/api/v1/roles`, { headers: authOnly(adminToken) })
.then((r) => r.json())
.then(async ({ roles }: { roles: Array<{ id: string; name: string }> }) => {
for (const r of roles) {
if (r.name === roleName) {
await fetch(`${API}/api/v1/roles/${r.id}`, {
method: "DELETE",
headers: authOnly(adminToken),
});
}
}
})
.catch(() => {});
}
});
test("built-in roles display their permissions list", async ({ loggedInPage: page }) => {
await openSettings(page);
await page.getByRole("button", { name: /^roles$/i }).click();
// Built-in roles should show permission badges (font-mono spans inside role cards)
await expect(page.locator(".font-mono").filter({ hasText: "tools:use" }).first()).toBeVisible();
});
});