mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
test: comprehensive GUI settings E2E including API Keys tab and full RBAC coverage
This commit is contained in:
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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: /github repository/i })).toBeVisible();
|
||||||
await expect(page.getByRole("link", { name: /documentation/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 });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -203,6 +203,83 @@ test.describe("GUI Settings - People Tab", () => {
|
|||||||
timeout: 5_000,
|
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", () => {
|
test.describe("GUI Settings - Teams Tab", () => {
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ async function deleteUser(adminToken: string, username: string): Promise<void> {
|
|||||||
// about - none
|
// about - none
|
||||||
|
|
||||||
base.describe("RBAC Settings Visibility - Admin", () => {
|
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 }) => {
|
base.test("admin sees all settings tabs including admin-only ones", async ({ page }) => {
|
||||||
await page.goto("/");
|
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: /audit log/i })).toBeVisible();
|
||||||
await expect(page.getByRole("button", { name: /ai features/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", () => {
|
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();
|
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", () => {
|
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();
|
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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -47,6 +47,30 @@ test.describe("GUI Settings - Security Tab", () => {
|
|||||||
|
|
||||||
await expect(currentPwInput).toHaveAttribute("type", "text");
|
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", () => {
|
test.describe("GUI Settings - API Keys Tab", () => {
|
||||||
|
|||||||
@@ -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.describe("GUI Settings - Product Analytics Tab", () => {
|
||||||
test("displays analytics consent section", async ({ loggedInPage: page }) => {
|
test("displays analytics consent section", async ({ loggedInPage: page }) => {
|
||||||
await page.locator("aside").getByText("Settings").click();
|
await page.locator("aside").getByText("Settings").click();
|
||||||
|
|||||||
Reference in New Issue
Block a user