mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
1. passport-photo 404 vs 501: add base route at /api/v1/tools/passport-photo
that returns 501 FEATURE_NOT_INSTALLED when the AI bundle is missing,
matching other AI tools. The /generate sub-route is Sharp-only (no
sidecar) so it correctly skips the isToolInstalled guard.
2. AuthGuard analytics consent race: don't evaluate shouldShowConsent()
until analyticsConfig has been fetched (guard on analyticsConfig !== null).
Prevents redirect to /analytics-consent before config is loaded.
3. Fragile sidebar Settings selector: add openSettings(page) helper to
E2E helpers that checks sidebar visibility with fallback to button role.
Replace all 134 occurrences of page.locator("aside").getByText("Settings")
across 18 test files.
417 lines
16 KiB
TypeScript
417 lines
16 KiB
TypeScript
import { expect, openSettings, test } from "./helpers";
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Settings Dialog -- General, System Settings, About tabs
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test.describe("GUI Settings - Dialog Navigation", () => {
|
|
test("opens settings dialog from sidebar", async ({ loggedInPage: page }) => {
|
|
await openSettings(page);
|
|
|
|
// Dialog sidebar should render with the Settings heading
|
|
await expect(page.locator("h2").filter({ hasText: "Settings" })).toBeVisible();
|
|
});
|
|
|
|
test("dialog sidebar lists navigable section tabs", async ({ loggedInPage: page }) => {
|
|
await openSettings(page);
|
|
|
|
// All section tabs should be buttons in the dialog sidebar
|
|
for (const label of [
|
|
"General",
|
|
"System Settings",
|
|
"Security",
|
|
"People",
|
|
"Teams",
|
|
"API Keys",
|
|
"Tools",
|
|
"About",
|
|
]) {
|
|
await expect(page.getByRole("button", { name: new RegExp(label, "i") })).toBeVisible();
|
|
}
|
|
});
|
|
|
|
test("clicking a tab switches the content pane", async ({ loggedInPage: page }) => {
|
|
await openSettings(page);
|
|
|
|
// Navigate to About section and confirm its heading
|
|
await page.getByRole("button", { name: /about/i }).click();
|
|
await expect(page.locator("h3").filter({ hasText: "About" })).toBeVisible();
|
|
|
|
// Navigate back to General and confirm its heading
|
|
await page.getByRole("button", { name: /general/i }).click();
|
|
await expect(page.locator("h3").filter({ hasText: "General" })).toBeVisible();
|
|
});
|
|
|
|
test("closes dialog via the X button", async ({ loggedInPage: page }) => {
|
|
await openSettings(page);
|
|
|
|
// The close button renders the X icon inside the dialog
|
|
const closeBtn = page.locator("button").filter({ has: page.locator("svg.lucide-x") });
|
|
await closeBtn.click();
|
|
|
|
// Dialog content should disappear
|
|
await expect(page.locator("h2").filter({ hasText: "Settings" })).not.toBeVisible();
|
|
});
|
|
|
|
test("closes dialog via Escape key", async ({ loggedInPage: page }) => {
|
|
await openSettings(page);
|
|
|
|
await page.keyboard.press("Escape");
|
|
|
|
await expect(page.locator("h2").filter({ hasText: "Settings" })).not.toBeVisible();
|
|
});
|
|
|
|
test("closes dialog by clicking the backdrop", async ({ loggedInPage: page }) => {
|
|
await openSettings(page);
|
|
|
|
// Click the backdrop overlay (the semi-transparent div behind the dialog)
|
|
await page.locator(".bg-black\\/50").click({ position: { x: 10, y: 10 } });
|
|
|
|
await expect(page.locator("h2").filter({ hasText: "Settings" })).not.toBeVisible();
|
|
});
|
|
});
|
|
|
|
test.describe("GUI Settings - General Tab", () => {
|
|
test("displays username and role badge", async ({ loggedInPage: page }) => {
|
|
await openSettings(page);
|
|
|
|
// General section is the default; username and role should be visible
|
|
await expect(page.getByText("admin").first()).toBeVisible();
|
|
// The role is displayed as a capitalize text below the username
|
|
await expect(page.getByText(/admin/i).first()).toBeVisible();
|
|
});
|
|
|
|
test("shows Default Tool View dropdown with options", async ({ loggedInPage: page }) => {
|
|
await openSettings(page);
|
|
|
|
await expect(page.getByText("Default Tool View")).toBeVisible();
|
|
const select = page.locator("select").first();
|
|
await expect(select).toBeVisible();
|
|
|
|
// Verify the two options
|
|
await expect(select.locator("option[value='sidebar']")).toHaveText("Sidebar");
|
|
await expect(select.locator("option[value='fullscreen']")).toHaveText("Fullscreen Grid");
|
|
});
|
|
|
|
test("shows App Version string", async ({ loggedInPage: page }) => {
|
|
await openSettings(page);
|
|
|
|
await expect(page.getByText("App Version")).toBeVisible();
|
|
// Version is in a monospace span matching semver pattern
|
|
await expect(page.locator(".font-mono").filter({ hasText: /^\d+\.\d+\.\d+/ })).toBeVisible();
|
|
});
|
|
|
|
test("has a Save Settings button", async ({ loggedInPage: page }) => {
|
|
await openSettings(page);
|
|
|
|
await expect(page.getByRole("button", { name: /save settings/i })).toBeVisible();
|
|
});
|
|
|
|
test("logout button redirects to /login", async ({ loggedInPage: page }) => {
|
|
await openSettings(page);
|
|
|
|
const logoutBtn = page.getByRole("button", { name: /log out/i });
|
|
await expect(logoutBtn).toBeVisible();
|
|
|
|
await logoutBtn.click();
|
|
await page.waitForURL("/login", { timeout: 10_000 });
|
|
|
|
// Should be on the login page
|
|
expect(page.url()).toContain("/login");
|
|
});
|
|
});
|
|
|
|
test.describe("GUI Settings - System Settings Tab", () => {
|
|
test("shows App Name input", async ({ loggedInPage: page }) => {
|
|
await openSettings(page);
|
|
await page.getByRole("button", { name: /system settings/i }).click();
|
|
|
|
await expect(page.getByText("App Name")).toBeVisible();
|
|
const appNameInput = page.locator("input[type='text']").first();
|
|
await expect(appNameInput).toBeVisible();
|
|
});
|
|
|
|
test("shows File Upload Limit input", async ({ loggedInPage: page }) => {
|
|
await openSettings(page);
|
|
await page.getByRole("button", { name: /system settings/i }).click();
|
|
|
|
await expect(page.getByText("File Upload Limit (MB)")).toBeVisible();
|
|
await expect(page.locator("input[type='number']").first()).toBeVisible();
|
|
});
|
|
|
|
test("shows Default Theme dropdown", async ({ loggedInPage: page }) => {
|
|
await openSettings(page);
|
|
await page.getByRole("button", { name: /system settings/i }).click();
|
|
|
|
await expect(page.getByText("Default Theme")).toBeVisible();
|
|
const themeSelect = page
|
|
.locator("select")
|
|
.filter({ has: page.locator("option[value='dark']") });
|
|
await expect(themeSelect).toBeVisible();
|
|
|
|
// Verify theme options
|
|
await expect(themeSelect.locator("option[value='light']")).toHaveText("Light");
|
|
await expect(themeSelect.locator("option[value='dark']")).toHaveText("Dark");
|
|
await expect(themeSelect.locator("option[value='system']")).toHaveText("System");
|
|
});
|
|
|
|
test("shows Login Attempt Limit input", async ({ loggedInPage: page }) => {
|
|
await openSettings(page);
|
|
await page.getByRole("button", { name: /system settings/i }).click();
|
|
|
|
await expect(page.getByText("Login Attempt Limit")).toBeVisible();
|
|
});
|
|
|
|
test("Save Settings button persists changes", async ({ loggedInPage: page }) => {
|
|
await openSettings(page);
|
|
await page.getByRole("button", { name: /system settings/i }).click();
|
|
|
|
// Wait for section to load
|
|
await expect(page.getByText("App Name")).toBeVisible();
|
|
|
|
// Click save
|
|
await page.getByRole("button", { name: /save settings/i }).click();
|
|
|
|
// Should show a success message
|
|
await expect(page.getByText("Settings saved.")).toBeVisible({ timeout: 5_000 });
|
|
});
|
|
});
|
|
|
|
test.describe("GUI Settings - About Tab", () => {
|
|
test("displays app description and version", async ({ loggedInPage: page }) => {
|
|
await openSettings(page);
|
|
await page.getByRole("button", { name: /about/i }).click();
|
|
|
|
await expect(page.locator("h3").filter({ hasText: "About" })).toBeVisible();
|
|
// SnapOtter branding
|
|
await expect(page.getByText("SnapOtter").first()).toBeVisible();
|
|
// Description text
|
|
await expect(page.getByText(/self-hosted.*privacy/i).first()).toBeVisible();
|
|
// Version label and value
|
|
await expect(page.getByText("Version:")).toBeVisible();
|
|
});
|
|
|
|
test("shows GitHub and documentation links", async ({ loggedInPage: page }) => {
|
|
await openSettings(page);
|
|
await page.getByRole("button", { name: /about/i }).click();
|
|
|
|
await expect(page.getByRole("link", { name: /github repository/i })).toBeVisible();
|
|
await expect(page.getByRole("link", { name: /documentation/i })).toBeVisible();
|
|
});
|
|
|
|
test("shows API Reference (Swagger) link", async ({ loggedInPage: page }) => {
|
|
await openSettings(page);
|
|
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 openSettings(page);
|
|
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 openSettings(page);
|
|
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 openSettings(page);
|
|
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 openSettings(page);
|
|
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 openSettings(page);
|
|
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 openSettings(page);
|
|
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 openSettings(page);
|
|
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 openSettings(page);
|
|
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 openSettings(page);
|
|
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 openSettings(page);
|
|
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 openSettings(page);
|
|
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 openSettings(page);
|
|
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 openSettings(page);
|
|
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 });
|
|
});
|
|
});
|