mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
fix: resolve 3 pre-existing issues found during test coverage expansion
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.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { expect, test } from "./helpers";
|
||||
import { expect, openSettings, test } from "./helpers";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// GUI Accessibility: ARIA semantics, focus management, keyboard navigation
|
||||
@@ -188,7 +188,7 @@ test.describe("Heading Hierarchy - Tool Pages", () => {
|
||||
// ---------------------------------------------------------------------------
|
||||
test.describe("Settings Dialog Accessibility", () => {
|
||||
test("settings dialog opens as a layered panel with backdrop", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
|
||||
// Dialog content should be visible
|
||||
await expect(page.locator("h2").filter({ hasText: "Settings" })).toBeVisible();
|
||||
@@ -199,8 +199,7 @@ test.describe("Settings Dialog Accessibility", () => {
|
||||
});
|
||||
|
||||
test("Escape key closes settings dialog", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await expect(page.locator("h2").filter({ hasText: "Settings" })).toBeVisible();
|
||||
await openSettings(page);
|
||||
|
||||
await page.keyboard.press("Escape");
|
||||
|
||||
@@ -208,8 +207,7 @@ test.describe("Settings Dialog Accessibility", () => {
|
||||
});
|
||||
|
||||
test("settings dialog can be closed via close button", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await expect(page.locator("h2").filter({ hasText: "Settings" })).toBeVisible();
|
||||
await openSettings(page);
|
||||
|
||||
// Use the X close button
|
||||
const closeBtn = page
|
||||
@@ -222,8 +220,7 @@ test.describe("Settings Dialog Accessibility", () => {
|
||||
});
|
||||
|
||||
test("settings dialog has a close button", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await expect(page.locator("h2").filter({ hasText: "Settings" })).toBeVisible();
|
||||
await openSettings(page);
|
||||
|
||||
// Close button with X icon
|
||||
const closeBtn = page.locator("button").filter({ has: page.locator("svg.lucide-x") });
|
||||
@@ -234,8 +231,7 @@ test.describe("Settings Dialog Accessibility", () => {
|
||||
});
|
||||
|
||||
test("settings dialog backdrop is marked aria-hidden", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await expect(page.locator("h2").filter({ hasText: "Settings" })).toBeVisible();
|
||||
await openSettings(page);
|
||||
|
||||
// The backdrop overlay has aria-hidden="true" to keep it out of the a11y tree
|
||||
const backdrop = page.locator("div[aria-hidden='true'].absolute.inset-0");
|
||||
@@ -245,8 +241,7 @@ test.describe("Settings Dialog Accessibility", () => {
|
||||
});
|
||||
|
||||
test("focus is contained inside settings dialog while open", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await expect(page.locator("h2").filter({ hasText: "Settings" })).toBeVisible();
|
||||
await openSettings(page);
|
||||
|
||||
// Tab through several elements -- focus should stay within the dialog
|
||||
for (let i = 0; i < 10; i++) {
|
||||
@@ -430,9 +425,7 @@ test.describe("Focus Management", () => {
|
||||
test.describe("Focus Management - Dialogs", () => {
|
||||
test("closing settings dialog returns focus to page", async ({ loggedInPage: page }) => {
|
||||
// Open settings
|
||||
const settingsBtn = page.locator("aside").getByText("Settings");
|
||||
await settingsBtn.click();
|
||||
await expect(page.locator("h2").filter({ hasText: "Settings" })).toBeVisible();
|
||||
await openSettings(page);
|
||||
|
||||
// Close via Escape
|
||||
await page.keyboard.press("Escape");
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { expect, type Page } from "@playwright/test";
|
||||
import { getTestImagePath, test, waitForProcessing } from "./helpers";
|
||||
import { getTestImagePath, openSettings, test, waitForProcessing } from "./helpers";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Cross-browser smoke tests -- critical flows validated across browsers.
|
||||
@@ -105,8 +105,7 @@ test.describe("Cross-browser smoke tests", () => {
|
||||
const errors = collectConsoleErrors(page);
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await expect(page.getByRole("heading", { name: "General" })).toBeVisible();
|
||||
await openSettings(page);
|
||||
|
||||
await page.getByRole("button", { name: "About" }).click();
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { expect, test, uploadTestImage } from "./helpers";
|
||||
import { expect, openSettings, test, uploadTestImage } from "./helpers";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Login Page (unauthenticated)
|
||||
@@ -345,7 +345,7 @@ test.describe("Sidebar Navigation", () => {
|
||||
});
|
||||
|
||||
test("Settings button opens SettingsDialog modal", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
|
||||
await expect(page.getByRole("heading", { name: "General" })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Security" })).toBeVisible();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { expect, test, uploadTestImage } from "./helpers";
|
||||
import { expect, openSettings, test, uploadTestImage } from "./helpers";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// GUI Performance: Page load budgets, SPA navigation, interaction responsiveness
|
||||
@@ -160,16 +160,14 @@ test.describe("Settings Dialog Timing", () => {
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
const start = Date.now();
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await page.locator("h2").filter({ hasText: "Settings" }).waitFor({ state: "visible" });
|
||||
await openSettings(page);
|
||||
const openTime = Date.now() - start;
|
||||
|
||||
expect(openTime).toBeLessThan(300);
|
||||
});
|
||||
|
||||
test("settings dialog closes within 300ms", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await page.locator("h2").filter({ hasText: "Settings" }).waitFor({ state: "visible" });
|
||||
await openSettings(page);
|
||||
|
||||
const start = Date.now();
|
||||
await page.keyboard.press("Escape");
|
||||
@@ -180,8 +178,7 @@ test.describe("Settings Dialog Timing", () => {
|
||||
});
|
||||
|
||||
test("switching settings tabs renders within 200ms", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await page.locator("h2").filter({ hasText: "Settings" }).waitFor({ state: "visible" });
|
||||
await openSettings(page);
|
||||
|
||||
// Switch to About tab
|
||||
const start = Date.now();
|
||||
@@ -362,7 +359,7 @@ test.describe("Repeated Operations Performance", () => {
|
||||
|
||||
for (let i = 0; i < 20; i++) {
|
||||
const start = Date.now();
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
await page.locator("h2").filter({ hasText: "Settings" }).waitFor({ state: "visible" });
|
||||
timings.push(Date.now() - start);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { expect, test, uploadTestImage, waitForProcessing } from "./helpers";
|
||||
import { expect, openSettings, test, uploadTestImage, waitForProcessing } from "./helpers";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// GUI Resilience: Error handling, form validation, state reset, stability
|
||||
@@ -187,10 +187,7 @@ test.describe("Change Password Form Validation", () => {
|
||||
test.describe("Add Member Form Validation", () => {
|
||||
test("adding a duplicate username shows error", async ({ loggedInPage: page }) => {
|
||||
// Open settings dialog and navigate to People section
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await expect(page.locator("h2").filter({ hasText: "Settings" })).toBeVisible({
|
||||
timeout: 5_000,
|
||||
});
|
||||
await openSettings(page);
|
||||
|
||||
// Navigate to People section
|
||||
await page.getByRole("button", { name: /people/i }).click();
|
||||
@@ -424,10 +421,7 @@ test.describe("Memory and Stability", () => {
|
||||
const start = Date.now();
|
||||
|
||||
// Open settings
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await expect(page.locator("h2").filter({ hasText: "Settings" })).toBeVisible({
|
||||
timeout: 5_000,
|
||||
});
|
||||
await openSettings(page);
|
||||
|
||||
const openTime = Date.now() - start;
|
||||
timings.push(openTime);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { expect, test } from "./helpers";
|
||||
import { expect, openSettings, test } from "./helpers";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Viewport definitions
|
||||
@@ -83,8 +83,7 @@ test.describe("Responsive - Desktop (1280x720)", () => {
|
||||
});
|
||||
|
||||
test("settings dialog fits within viewport", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await expect(page.getByRole("heading", { name: "General" })).toBeVisible();
|
||||
await openSettings(page);
|
||||
|
||||
const dialogBox = page.locator("[class*='max-w']").filter({ hasText: "General" }).first();
|
||||
const box = await dialogBox.boundingBox();
|
||||
@@ -168,12 +167,7 @@ test.describe("Responsive - Tablet (768x1024)", () => {
|
||||
});
|
||||
|
||||
test("settings dialog fits within tablet viewport", async ({ loggedInPage: page }) => {
|
||||
const sidebar = page.locator("aside");
|
||||
// On tablet, sidebar may or may not be visible; use it if visible, otherwise use menu
|
||||
if (await sidebar.isVisible({ timeout: 2000 }).catch(() => false)) {
|
||||
await sidebar.getByText("Settings").click();
|
||||
}
|
||||
await expect(page.getByRole("heading", { name: "General" })).toBeVisible({ timeout: 10_000 });
|
||||
await openSettings(page);
|
||||
|
||||
const scrollWidth = await page.evaluate(() => document.documentElement.scrollWidth);
|
||||
const clientWidth = await page.evaluate(() => document.documentElement.clientWidth);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { expect, test } from "./helpers";
|
||||
import { expect, openSettings, test } from "./helpers";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Settings Dialog -- API Keys tab (full coverage)
|
||||
@@ -6,7 +6,7 @@ import { expect, test } from "./helpers";
|
||||
|
||||
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 openSettings(page);
|
||||
await page.getByRole("button", { name: /api keys/i }).click();
|
||||
|
||||
await expect(page.locator("h3").filter({ hasText: "API Keys" })).toBeVisible();
|
||||
@@ -14,7 +14,7 @@ test.describe("GUI Settings - API Keys Tab", () => {
|
||||
});
|
||||
|
||||
test("shows Generate API Key button and name input", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
await page.getByRole("button", { name: /api keys/i }).click();
|
||||
|
||||
await expect(page.getByPlaceholder("Key name (optional)")).toBeVisible();
|
||||
@@ -24,7 +24,7 @@ test.describe("GUI Settings - API Keys Tab", () => {
|
||||
test("generating an API key displays the key with copy button", async ({
|
||||
loggedInPage: page,
|
||||
}) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
await page.getByRole("button", { name: /api keys/i }).click();
|
||||
|
||||
const keyName = `apiKeyTest-${Date.now()}`;
|
||||
@@ -50,7 +50,7 @@ test.describe("GUI Settings - API Keys Tab", () => {
|
||||
});
|
||||
|
||||
test("generated key appears in Existing Keys list", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
await page.getByRole("button", { name: /api keys/i }).click();
|
||||
|
||||
const keyName = `listTest-${Date.now()}`;
|
||||
@@ -71,7 +71,7 @@ test.describe("GUI Settings - API Keys Tab", () => {
|
||||
});
|
||||
|
||||
test("permission scoping toggle reveals checkboxes", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
await page.getByRole("button", { name: /api keys/i }).click();
|
||||
|
||||
const scopingToggle = page.getByText("Restrict permissions (optional)");
|
||||
@@ -86,7 +86,7 @@ test.describe("GUI Settings - API Keys Tab", () => {
|
||||
});
|
||||
|
||||
test("permission scoping checkboxes can be toggled", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
await page.getByRole("button", { name: /api keys/i }).click();
|
||||
|
||||
// Open scoping panel
|
||||
@@ -105,7 +105,7 @@ test.describe("GUI Settings - API Keys Tab", () => {
|
||||
});
|
||||
|
||||
test("deleting an API key removes it from the list", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
await page.getByRole("button", { name: /api keys/i }).click();
|
||||
|
||||
// Create a key to delete
|
||||
@@ -126,7 +126,7 @@ test.describe("GUI Settings - API Keys Tab", () => {
|
||||
});
|
||||
|
||||
test("expiration date input is available", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
await page.getByRole("button", { name: /api keys/i }).click();
|
||||
|
||||
await expect(page.getByText("Expires:")).toBeVisible();
|
||||
@@ -134,7 +134,7 @@ test.describe("GUI Settings - API Keys Tab", () => {
|
||||
});
|
||||
|
||||
test("expiration date Clear button works", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
await page.getByRole("button", { name: /api keys/i }).click();
|
||||
|
||||
const dateInput = page.locator("input[type='datetime-local']");
|
||||
@@ -152,7 +152,7 @@ test.describe("GUI Settings - API Keys Tab", () => {
|
||||
test("empty state shows no keys message or existing keys list", async ({
|
||||
loggedInPage: page,
|
||||
}) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
await page.getByRole("button", { name: /api keys/i }).click();
|
||||
|
||||
// Wait for the API Keys heading to confirm the section loaded
|
||||
@@ -176,7 +176,7 @@ test.describe("GUI Settings - API Keys Tab", () => {
|
||||
test("generating a key with scoped permissions creates a scoped key", async ({
|
||||
loggedInPage: page,
|
||||
}) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
await page.getByRole("button", { name: /api keys/i }).click();
|
||||
|
||||
// Set a key name
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { expect, test } from "./helpers";
|
||||
import { expect, openSettings, test } from "./helpers";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Settings Dialog -- General, System Settings, About tabs
|
||||
@@ -6,14 +6,14 @@ import { expect, test } from "./helpers";
|
||||
|
||||
test.describe("GUI Settings - Dialog Navigation", () => {
|
||||
test("opens settings dialog from sidebar", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
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 page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
|
||||
// All section tabs should be buttons in the dialog sidebar
|
||||
for (const label of [
|
||||
@@ -31,7 +31,7 @@ test.describe("GUI Settings - Dialog Navigation", () => {
|
||||
});
|
||||
|
||||
test("clicking a tab switches the content pane", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
|
||||
// Navigate to About section and confirm its heading
|
||||
await page.getByRole("button", { name: /about/i }).click();
|
||||
@@ -43,8 +43,7 @@ test.describe("GUI Settings - Dialog Navigation", () => {
|
||||
});
|
||||
|
||||
test("closes dialog via the X button", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await expect(page.locator("h2").filter({ hasText: "Settings" })).toBeVisible();
|
||||
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") });
|
||||
@@ -55,8 +54,7 @@ test.describe("GUI Settings - Dialog Navigation", () => {
|
||||
});
|
||||
|
||||
test("closes dialog via Escape key", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await expect(page.locator("h2").filter({ hasText: "Settings" })).toBeVisible();
|
||||
await openSettings(page);
|
||||
|
||||
await page.keyboard.press("Escape");
|
||||
|
||||
@@ -64,8 +62,7 @@ test.describe("GUI Settings - Dialog Navigation", () => {
|
||||
});
|
||||
|
||||
test("closes dialog by clicking the backdrop", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await expect(page.locator("h2").filter({ hasText: "Settings" })).toBeVisible();
|
||||
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 } });
|
||||
@@ -76,7 +73,7 @@ test.describe("GUI Settings - Dialog Navigation", () => {
|
||||
|
||||
test.describe("GUI Settings - General Tab", () => {
|
||||
test("displays username and role badge", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
|
||||
// General section is the default; username and role should be visible
|
||||
await expect(page.getByText("admin").first()).toBeVisible();
|
||||
@@ -85,7 +82,7 @@ test.describe("GUI Settings - General Tab", () => {
|
||||
});
|
||||
|
||||
test("shows Default Tool View dropdown with options", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
|
||||
await expect(page.getByText("Default Tool View")).toBeVisible();
|
||||
const select = page.locator("select").first();
|
||||
@@ -97,7 +94,7 @@ test.describe("GUI Settings - General Tab", () => {
|
||||
});
|
||||
|
||||
test("shows App Version string", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
|
||||
await expect(page.getByText("App Version")).toBeVisible();
|
||||
// Version is in a monospace span matching semver pattern
|
||||
@@ -105,13 +102,13 @@ test.describe("GUI Settings - General Tab", () => {
|
||||
});
|
||||
|
||||
test("has a Save Settings button", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
|
||||
await expect(page.getByRole("button", { name: /save settings/i })).toBeVisible();
|
||||
});
|
||||
|
||||
test("logout button redirects to /login", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
|
||||
const logoutBtn = page.getByRole("button", { name: /log out/i });
|
||||
await expect(logoutBtn).toBeVisible();
|
||||
@@ -126,7 +123,7 @@ test.describe("GUI Settings - General Tab", () => {
|
||||
|
||||
test.describe("GUI Settings - System Settings Tab", () => {
|
||||
test("shows App Name input", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
await page.getByRole("button", { name: /system settings/i }).click();
|
||||
|
||||
await expect(page.getByText("App Name")).toBeVisible();
|
||||
@@ -135,7 +132,7 @@ test.describe("GUI Settings - System Settings Tab", () => {
|
||||
});
|
||||
|
||||
test("shows File Upload Limit input", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
await page.getByRole("button", { name: /system settings/i }).click();
|
||||
|
||||
await expect(page.getByText("File Upload Limit (MB)")).toBeVisible();
|
||||
@@ -143,7 +140,7 @@ test.describe("GUI Settings - System Settings Tab", () => {
|
||||
});
|
||||
|
||||
test("shows Default Theme dropdown", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
await page.getByRole("button", { name: /system settings/i }).click();
|
||||
|
||||
await expect(page.getByText("Default Theme")).toBeVisible();
|
||||
@@ -159,14 +156,14 @@ test.describe("GUI Settings - System Settings Tab", () => {
|
||||
});
|
||||
|
||||
test("shows Login Attempt Limit input", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
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 page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
await page.getByRole("button", { name: /system settings/i }).click();
|
||||
|
||||
// Wait for section to load
|
||||
@@ -182,7 +179,7 @@ test.describe("GUI Settings - System Settings Tab", () => {
|
||||
|
||||
test.describe("GUI Settings - About Tab", () => {
|
||||
test("displays app description and version", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
await page.getByRole("button", { name: /about/i }).click();
|
||||
|
||||
await expect(page.locator("h3").filter({ hasText: "About" })).toBeVisible();
|
||||
@@ -195,7 +192,7 @@ test.describe("GUI Settings - About Tab", () => {
|
||||
});
|
||||
|
||||
test("shows GitHub and documentation links", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
await page.getByRole("button", { name: /about/i }).click();
|
||||
|
||||
await expect(page.getByRole("link", { name: /github repository/i })).toBeVisible();
|
||||
@@ -203,14 +200,14 @@ test.describe("GUI Settings - About Tab", () => {
|
||||
});
|
||||
|
||||
test("shows API Reference (Swagger) link", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
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 page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
await page.getByRole("button", { name: /about/i }).click();
|
||||
|
||||
// Version text should match semver format (e.g., 1.2.3)
|
||||
@@ -219,7 +216,7 @@ test.describe("GUI Settings - About Tab", () => {
|
||||
});
|
||||
|
||||
test("about section has the Links heading", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
await page.getByRole("button", { name: /about/i }).click();
|
||||
|
||||
await expect(page.getByText("Links")).toBeVisible();
|
||||
@@ -228,7 +225,7 @@ test.describe("GUI Settings - About Tab", () => {
|
||||
|
||||
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 openSettings(page);
|
||||
await page.getByRole("button", { name: /ai features/i }).click();
|
||||
|
||||
await expect(page.locator("h3").filter({ hasText: "AI Features" })).toBeVisible();
|
||||
@@ -236,14 +233,14 @@ test.describe("GUI Settings - AI Features Tab", () => {
|
||||
});
|
||||
|
||||
test("shows Install All button", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
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 page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
await page.getByRole("button", { name: /ai features/i }).click();
|
||||
|
||||
// Each bundle card is rendered within a bordered div
|
||||
@@ -272,7 +269,7 @@ test.describe("GUI Settings - AI Features Tab", () => {
|
||||
});
|
||||
|
||||
test("bundles show estimated size", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
await page.getByRole("button", { name: /ai features/i }).click();
|
||||
|
||||
// Each bundle should display an estimated size like (~XXX MB)
|
||||
@@ -282,7 +279,7 @@ test.describe("GUI Settings - AI Features Tab", () => {
|
||||
|
||||
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 openSettings(page);
|
||||
await page.getByRole("button", { name: /tools/i }).click();
|
||||
|
||||
await expect(page.locator("h3").filter({ hasText: "Tools" }).first()).toBeVisible();
|
||||
@@ -297,7 +294,7 @@ test.describe("GUI Settings - Tools Tab (deep)", () => {
|
||||
});
|
||||
|
||||
test("toggling a tool changes the disabled counter", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
await page.getByRole("button", { name: /tools/i }).click();
|
||||
|
||||
// Read initial disabled count
|
||||
@@ -320,7 +317,7 @@ test.describe("GUI Settings - Tools Tab (deep)", () => {
|
||||
});
|
||||
|
||||
test("search filters tools in settings dialog", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
await page.getByRole("button", { name: /tools/i }).click();
|
||||
|
||||
// Wait for the tools to load
|
||||
@@ -353,7 +350,7 @@ 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 openSettings(page);
|
||||
await page.getByRole("button", { name: /product analytics/i }).click();
|
||||
|
||||
await expect(page.getByText(/share anonymous usage data/i)).toBeVisible();
|
||||
@@ -361,7 +358,7 @@ test.describe("GUI Settings - Product Analytics Tab (deep)", () => {
|
||||
});
|
||||
|
||||
test("shows either consent toggle or admin-disabled message", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
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
|
||||
@@ -381,7 +378,7 @@ test.describe("GUI Settings - Save and Persistence", () => {
|
||||
loggedInPage: page,
|
||||
}) => {
|
||||
// Navigate to System Settings tab
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
await page.getByRole("button", { name: /system settings/i }).click();
|
||||
await expect(page.getByText("App Name")).toBeVisible({ timeout: 5_000 });
|
||||
|
||||
@@ -403,7 +400,7 @@ test.describe("GUI Settings - Save and Persistence", () => {
|
||||
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 openSettings(page);
|
||||
await page.getByRole("button", { name: /system settings/i }).click();
|
||||
await expect(page.getByText("App Name")).toBeVisible();
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { expect, test } from "./helpers";
|
||||
import { expect, openSettings, test } from "./helpers";
|
||||
|
||||
const API = process.env.API_URL || "http://localhost:13490";
|
||||
|
||||
@@ -64,7 +64,7 @@ const UID = Date.now().toString(36);
|
||||
|
||||
test.describe("GUI Settings - People Tab", () => {
|
||||
test("displays user count and user table", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
await page.getByRole("button", { name: /people/i }).click();
|
||||
|
||||
// User count
|
||||
@@ -80,7 +80,7 @@ test.describe("GUI Settings - People Tab", () => {
|
||||
});
|
||||
|
||||
test("search filters users and shows empty state", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
await page.getByRole("button", { name: /people/i }).click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
@@ -96,7 +96,7 @@ test.describe("GUI Settings - People Tab", () => {
|
||||
test("Add Members opens form with username, password, role, team fields", async ({
|
||||
loggedInPage: page,
|
||||
}) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
await page.getByRole("button", { name: /people/i }).click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
@@ -124,7 +124,7 @@ test.describe("GUI Settings - People Tab", () => {
|
||||
let adminToken: string;
|
||||
|
||||
try {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
await page.getByRole("button", { name: /people/i }).click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
@@ -155,7 +155,7 @@ test.describe("GUI Settings - People Tab", () => {
|
||||
});
|
||||
|
||||
// Try to create same username via GUI
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
await page.getByRole("button", { name: /people/i }).click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
@@ -174,7 +174,7 @@ test.describe("GUI Settings - People Tab", () => {
|
||||
test("three-dot menu shows Edit Role/Team, Reset Password, and Delete", async ({
|
||||
loggedInPage: page,
|
||||
}) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
await page.getByRole("button", { name: /people/i }).click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
@@ -187,7 +187,7 @@ test.describe("GUI Settings - People Tab", () => {
|
||||
});
|
||||
|
||||
test("cannot delete yourself via the menu", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
await page.getByRole("button", { name: /people/i }).click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
@@ -216,7 +216,7 @@ test.describe("GUI Settings - People Tab", () => {
|
||||
body: JSON.stringify({ username, password: "TestPass123!", role: "user" }),
|
||||
});
|
||||
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
await page.getByRole("button", { name: /people/i }).click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
@@ -238,7 +238,7 @@ test.describe("GUI Settings - People Tab", () => {
|
||||
});
|
||||
|
||||
test("cannot demote your own admin role via Edit Role / Team", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
await page.getByRole("button", { name: /people/i }).click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
@@ -263,7 +263,7 @@ test.describe("GUI Settings - People Tab", () => {
|
||||
});
|
||||
|
||||
test("Reset Password opens the reset form for a user", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
await page.getByRole("button", { name: /people/i }).click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
@@ -284,7 +284,7 @@ test.describe("GUI Settings - People Tab", () => {
|
||||
|
||||
test.describe("GUI Settings - Teams Tab", () => {
|
||||
test("displays team list with Default team", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
await page.getByRole("button", { name: /teams/i }).click();
|
||||
|
||||
await expect(page.locator("h3").filter({ hasText: "Teams" })).toBeVisible();
|
||||
@@ -300,7 +300,7 @@ test.describe("GUI Settings - Teams Tab", () => {
|
||||
const adminToken = await getAdminToken();
|
||||
|
||||
try {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
await page.getByRole("button", { name: /teams/i }).click();
|
||||
|
||||
await page.getByRole("button", { name: /create new team/i }).click();
|
||||
@@ -328,7 +328,7 @@ test.describe("GUI Settings - Teams Tab", () => {
|
||||
body: JSON.stringify({ name: teamName }),
|
||||
});
|
||||
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
await page.getByRole("button", { name: /teams/i }).click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
@@ -353,7 +353,7 @@ test.describe("GUI Settings - Teams Tab", () => {
|
||||
|
||||
test.describe("GUI Settings - Roles Tab", () => {
|
||||
test("displays built-in roles with Built-in badge", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
await page.getByRole("button", { name: /^roles$/i }).click();
|
||||
|
||||
await expect(page.locator("h3").filter({ hasText: "Roles" })).toBeVisible();
|
||||
@@ -369,7 +369,7 @@ test.describe("GUI Settings - Roles Tab", () => {
|
||||
});
|
||||
|
||||
test("built-in roles do not show edit or delete buttons", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
await page.getByRole("button", { name: /^roles$/i }).click();
|
||||
|
||||
await expect(page.getByText("Built-in").first()).toBeVisible();
|
||||
@@ -386,7 +386,7 @@ test.describe("GUI Settings - Roles Tab", () => {
|
||||
});
|
||||
|
||||
test("Create Custom Role button is visible", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
await page.getByRole("button", { name: /^roles$/i }).click();
|
||||
|
||||
await expect(page.getByRole("button", { name: /create custom role/i })).toBeVisible();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { test as base, expect } from "@playwright/test";
|
||||
import { login } from "./helpers";
|
||||
import { login, openSettings } from "./helpers";
|
||||
|
||||
const API = process.env.API_URL || "http://localhost:13490";
|
||||
|
||||
@@ -116,7 +116,7 @@ base.describe("RBAC Settings Visibility - Admin", () => {
|
||||
|
||||
base.test("admin sees all settings tabs including admin-only ones", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
|
||||
// Tabs visible to all roles
|
||||
await expect(page.getByRole("button", { name: /general/i })).toBeVisible();
|
||||
@@ -137,7 +137,7 @@ base.describe("RBAC Settings Visibility - Admin", () => {
|
||||
|
||||
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 openSettings(page);
|
||||
await page.getByRole("button", { name: /people/i }).click();
|
||||
|
||||
await expect(page.getByText(/\d+ users?/)).toBeVisible({ timeout: 5_000 });
|
||||
@@ -146,7 +146,7 @@ base.describe("RBAC Settings Visibility - Admin", () => {
|
||||
|
||||
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 openSettings(page);
|
||||
await page.getByRole("button", { name: /audit log/i }).click();
|
||||
|
||||
await expect(page.locator("h3").filter({ hasText: "Audit Log" })).toBeVisible();
|
||||
@@ -158,7 +158,7 @@ base.describe("RBAC Settings Visibility - Admin", () => {
|
||||
|
||||
base.test("admin sees all 12 nav items", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
|
||||
// Count the navigation buttons in the settings dialog sidebar
|
||||
const navButtons = page.locator(".w-48 button");
|
||||
@@ -183,7 +183,7 @@ base.describe("RBAC Settings Visibility - Editor", () => {
|
||||
"editor sees general, security, api-keys, tools, analytics, about",
|
||||
async ({ page }) => {
|
||||
await login(page, EDITOR_USER, EDITOR_PASS);
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
|
||||
// Should see these tabs
|
||||
await expect(page.getByRole("button", { name: /general/i })).toBeVisible();
|
||||
@@ -199,7 +199,7 @@ base.describe("RBAC Settings Visibility - Editor", () => {
|
||||
"editor does NOT see system settings, people, teams, roles, audit log, ai features",
|
||||
async ({ page }) => {
|
||||
await login(page, EDITOR_USER, EDITOR_PASS);
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
|
||||
// Wait for dialog to fully render
|
||||
await expect(page.getByRole("button", { name: /general/i })).toBeVisible();
|
||||
@@ -216,7 +216,7 @@ base.describe("RBAC Settings Visibility - Editor", () => {
|
||||
|
||||
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 openSettings(page);
|
||||
await expect(page.getByRole("button", { name: /general/i })).toBeVisible();
|
||||
|
||||
const navButtons = page.locator(".w-48 button");
|
||||
@@ -226,7 +226,7 @@ base.describe("RBAC Settings Visibility - Editor", () => {
|
||||
|
||||
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 openSettings(page);
|
||||
await page.getByRole("button", { name: /security/i }).click();
|
||||
|
||||
await expect(page.getByText("Change Password").first()).toBeVisible();
|
||||
@@ -235,7 +235,7 @@ base.describe("RBAC Settings Visibility - Editor", () => {
|
||||
|
||||
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 openSettings(page);
|
||||
await page.getByRole("button", { name: /api keys/i }).click();
|
||||
|
||||
await expect(page.getByRole("button", { name: /generate api key/i })).toBeVisible();
|
||||
@@ -256,7 +256,7 @@ base.describe("RBAC Settings Visibility - User", () => {
|
||||
|
||||
base.test("user sees general, security, api-keys, tools, analytics, about", async ({ page }) => {
|
||||
await login(page, USER_USER, USER_PASS);
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
|
||||
await expect(page.getByRole("button", { name: /general/i })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: /security/i })).toBeVisible();
|
||||
@@ -270,7 +270,7 @@ base.describe("RBAC Settings Visibility - User", () => {
|
||||
"user does NOT see system settings, people, teams, roles, audit log, ai features",
|
||||
async ({ page }) => {
|
||||
await login(page, USER_USER, USER_PASS);
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
|
||||
// Wait for dialog to fully render
|
||||
await expect(page.getByRole("button", { name: /general/i })).toBeVisible();
|
||||
@@ -286,7 +286,7 @@ base.describe("RBAC Settings Visibility - User", () => {
|
||||
|
||||
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 openSettings(page);
|
||||
await expect(page.getByRole("button", { name: /general/i })).toBeVisible();
|
||||
|
||||
const navButtons = page.locator(".w-48 button");
|
||||
@@ -296,7 +296,7 @@ base.describe("RBAC Settings Visibility - User", () => {
|
||||
|
||||
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 openSettings(page);
|
||||
await page.getByRole("button", { name: /about/i }).click();
|
||||
|
||||
await expect(page.locator("h3").filter({ hasText: "About" })).toBeVisible();
|
||||
@@ -305,7 +305,7 @@ base.describe("RBAC Settings Visibility - User", () => {
|
||||
|
||||
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();
|
||||
await openSettings(page);
|
||||
|
||||
// General is the default tab; should show the user's username and role
|
||||
await expect(page.getByText(USER_USER)).toBeVisible({ timeout: 5_000 });
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { expect, test } from "./helpers";
|
||||
import { expect, openSettings, test } from "./helpers";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Settings Dialog -- Security (change password) and API Keys tabs
|
||||
@@ -6,7 +6,7 @@ import { expect, test } from "./helpers";
|
||||
|
||||
test.describe("GUI Settings - Security Tab", () => {
|
||||
test("shows Change Password form with required inputs", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
await page.getByRole("button", { name: /security/i }).click();
|
||||
|
||||
await expect(page.locator("h3").filter({ hasText: "Security" })).toBeVisible();
|
||||
@@ -22,7 +22,7 @@ test.describe("GUI Settings - Security Tab", () => {
|
||||
});
|
||||
|
||||
test("mismatched passwords show error message", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
await page.getByRole("button", { name: /security/i }).click();
|
||||
|
||||
await page.getByPlaceholder("Current Password").fill("admin");
|
||||
@@ -35,7 +35,7 @@ test.describe("GUI Settings - Security Tab", () => {
|
||||
});
|
||||
|
||||
test("password visibility toggles work", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
await page.getByRole("button", { name: /security/i }).click();
|
||||
|
||||
const currentPwInput = page.getByPlaceholder("Current Password");
|
||||
@@ -49,7 +49,7 @@ test.describe("GUI Settings - Security Tab", () => {
|
||||
});
|
||||
|
||||
test("short password shows validation error", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
await page.getByRole("button", { name: /security/i }).click();
|
||||
|
||||
await page.getByPlaceholder("Current Password").fill("admin");
|
||||
@@ -63,7 +63,7 @@ test.describe("GUI Settings - Security Tab", () => {
|
||||
});
|
||||
|
||||
test("security section shows login attempt limit reference", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
await page.getByRole("button", { name: /security/i }).click();
|
||||
|
||||
await expect(page.getByText(/login attempt limits/i)).toBeVisible();
|
||||
@@ -75,7 +75,7 @@ test.describe("GUI Settings - Security Tab", () => {
|
||||
|
||||
test.describe("GUI Settings - API Keys Tab", () => {
|
||||
test("shows Generate API Key button and name input", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
await page.getByRole("button", { name: /api keys/i }).click();
|
||||
|
||||
await expect(page.locator("h3").filter({ hasText: "API Keys" })).toBeVisible();
|
||||
@@ -84,7 +84,7 @@ test.describe("GUI Settings - API Keys Tab", () => {
|
||||
});
|
||||
|
||||
test("generating an API key displays the key once", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
await page.getByRole("button", { name: /api keys/i }).click();
|
||||
|
||||
// Give a unique name so we can identify and clean it up
|
||||
@@ -108,7 +108,7 @@ test.describe("GUI Settings - API Keys Tab", () => {
|
||||
});
|
||||
|
||||
test("generated key appears in existing keys list", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
await page.getByRole("button", { name: /api keys/i }).click();
|
||||
|
||||
const keyName = `listTest-${Date.now()}`;
|
||||
@@ -129,7 +129,7 @@ test.describe("GUI Settings - API Keys Tab", () => {
|
||||
});
|
||||
|
||||
test("permission scoping toggle reveals checkboxes", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
await page.getByRole("button", { name: /api keys/i }).click();
|
||||
|
||||
const scopingToggle = page.getByText("Restrict permissions (optional)");
|
||||
@@ -144,7 +144,7 @@ test.describe("GUI Settings - API Keys Tab", () => {
|
||||
});
|
||||
|
||||
test("deleting an API key removes it from the list", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
await page.getByRole("button", { name: /api keys/i }).click();
|
||||
|
||||
// Create a key to delete
|
||||
@@ -165,7 +165,7 @@ test.describe("GUI Settings - API Keys Tab", () => {
|
||||
});
|
||||
|
||||
test("expiration date input is available", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
await page.getByRole("button", { name: /api keys/i }).click();
|
||||
|
||||
await expect(page.getByText("Expires:")).toBeVisible();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { expect, test } from "./helpers";
|
||||
import { expect, openSettings, test } from "./helpers";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Settings Dialog -- Tools tab (enable/disable) and Product Analytics
|
||||
@@ -6,7 +6,7 @@ import { expect, test } from "./helpers";
|
||||
|
||||
test.describe("GUI Settings - Tools Tab", () => {
|
||||
test("displays tools list with category headings", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
await page.getByRole("button", { name: /tools/i }).click();
|
||||
|
||||
await expect(page.locator("h3").filter({ hasText: "Tools" }).first()).toBeVisible();
|
||||
@@ -18,7 +18,7 @@ test.describe("GUI Settings - Tools Tab", () => {
|
||||
});
|
||||
|
||||
test("Save Tool Settings button and disabled tools counter", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
await page.getByRole("button", { name: /tools/i }).click();
|
||||
|
||||
await expect(page.getByRole("button", { name: /save tool settings/i })).toBeVisible();
|
||||
@@ -28,7 +28,7 @@ test.describe("GUI Settings - Tools Tab", () => {
|
||||
});
|
||||
|
||||
test("saving tool settings shows restart banner", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
await page.getByRole("button", { name: /tools/i }).click();
|
||||
|
||||
await page.getByRole("button", { name: /save tool settings/i }).click();
|
||||
@@ -41,7 +41,7 @@ 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 openSettings(page);
|
||||
await page.getByRole("button", { name: /tools/i }).click();
|
||||
|
||||
await expect(page.locator("h3").filter({ hasText: "Tools" }).first()).toBeVisible();
|
||||
@@ -56,7 +56,7 @@ test.describe("GUI Settings - Tools Tab (additional)", () => {
|
||||
});
|
||||
|
||||
test("tools show both name and description", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
await page.getByRole("button", { name: /tools/i }).click();
|
||||
|
||||
// Wait for tools to load
|
||||
@@ -70,7 +70,7 @@ test.describe("GUI Settings - Tools Tab (additional)", () => {
|
||||
|
||||
test.describe("GUI Settings - Product Analytics Tab", () => {
|
||||
test("displays analytics consent section", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
await page.getByRole("button", { name: /product analytics/i }).click();
|
||||
|
||||
await expect(page.getByText("Product Analytics").first()).toBeVisible();
|
||||
@@ -78,7 +78,7 @@ test.describe("GUI Settings - Product Analytics Tab", () => {
|
||||
});
|
||||
|
||||
test("analytics toggle is present", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
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
|
||||
@@ -93,7 +93,7 @@ test.describe("GUI Settings - Product Analytics Tab", () => {
|
||||
});
|
||||
|
||||
test("privacy policy link is present", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
await page.getByRole("button", { name: /product analytics/i }).click();
|
||||
|
||||
await expect(page.getByRole("link", { name: /learn more/i })).toBeVisible();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { expect, test, uploadTestImage } from "./helpers";
|
||||
import { expect, openSettings, test, uploadTestImage } from "./helpers";
|
||||
|
||||
const isDocker = process.env.CI === "true" || process.env.DOCKER === "true";
|
||||
|
||||
@@ -113,8 +113,7 @@ test.describe("Visual Desktop (1280x720)", () => {
|
||||
|
||||
// ---- Settings dialog - General tab ----
|
||||
test("settings dialog general tab - light and dark", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await expect(page.getByRole("heading", { name: "General" })).toBeVisible();
|
||||
await openSettings(page);
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await takeThemedScreenshots(page, "settings-general");
|
||||
@@ -122,8 +121,7 @@ test.describe("Visual Desktop (1280x720)", () => {
|
||||
|
||||
// ---- Settings dialog - About tab ----
|
||||
test("settings dialog about tab - light and dark", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await expect(page.getByRole("heading", { name: "General" })).toBeVisible();
|
||||
await openSettings(page);
|
||||
|
||||
// Navigate to About tab
|
||||
await page.getByRole("button", { name: "About" }).click();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { expect, test, uploadTestImage } from "./helpers";
|
||||
import { expect, openSettings, test, uploadTestImage } from "./helpers";
|
||||
|
||||
const isDocker = process.env.CI === "true" || process.env.DOCKER === "true";
|
||||
const MOD = process.platform === "darwin" ? "Meta" : "Control";
|
||||
@@ -96,19 +96,7 @@ test.describe("Visual Tablet (768x1024)", () => {
|
||||
|
||||
// ---- Settings dialog - General tab ----
|
||||
test("settings dialog general tab - light and dark", async ({ loggedInPage: page }) => {
|
||||
// On tablet, the sidebar may or may not be visible; try sidebar first, fallback to nav
|
||||
const sidebar = page.locator("aside");
|
||||
const sidebarVisible = await sidebar.isVisible().catch(() => false);
|
||||
|
||||
if (sidebarVisible) {
|
||||
await sidebar.getByText("Settings").click();
|
||||
} else {
|
||||
// Fallback: bottom nav bar
|
||||
const bottomNav = page.locator("nav.fixed");
|
||||
await bottomNav.getByText("Settings").click();
|
||||
}
|
||||
|
||||
await expect(page.getByRole("heading", { name: "General" })).toBeVisible();
|
||||
await openSettings(page);
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await takeThemedScreenshots(page, "settings-general");
|
||||
@@ -116,17 +104,7 @@ test.describe("Visual Tablet (768x1024)", () => {
|
||||
|
||||
// ---- Settings dialog - About tab ----
|
||||
test("settings dialog about tab - light and dark", async ({ loggedInPage: page }) => {
|
||||
const sidebar = page.locator("aside");
|
||||
const sidebarVisible = await sidebar.isVisible().catch(() => false);
|
||||
|
||||
if (sidebarVisible) {
|
||||
await sidebar.getByText("Settings").click();
|
||||
} else {
|
||||
const bottomNav = page.locator("nav.fixed");
|
||||
await bottomNav.getByText("Settings").click();
|
||||
}
|
||||
|
||||
await expect(page.getByRole("heading", { name: "General" })).toBeVisible();
|
||||
await openSettings(page);
|
||||
|
||||
// Navigate to About tab
|
||||
await page.getByRole("button", { name: "About" }).click();
|
||||
|
||||
@@ -165,4 +165,17 @@ export async function isAiSidecarRunning(page: Page): Promise<boolean> {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// openSettings() — reliably open the Settings dialog across all viewports
|
||||
// ---------------------------------------------------------------------------
|
||||
export async function openSettings(page: Page): Promise<void> {
|
||||
const sidebar = page.locator("aside");
|
||||
if (await sidebar.isVisible({ timeout: 2000 }).catch(() => false)) {
|
||||
await sidebar.getByText("Settings").click();
|
||||
} else {
|
||||
await page.getByRole("button", { name: /settings/i }).click();
|
||||
}
|
||||
await page.getByRole("dialog").waitFor({ state: "visible", timeout: 5000 });
|
||||
}
|
||||
|
||||
export { expect };
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { expect, test } from "./helpers";
|
||||
import { expect, openSettings, test } from "./helpers";
|
||||
|
||||
test.describe("Navigation", () => {
|
||||
test("sidebar Tools link goes to home", async ({ loggedInPage: page }) => {
|
||||
@@ -26,7 +26,7 @@ test.describe("Navigation", () => {
|
||||
});
|
||||
|
||||
test("sidebar Settings button opens settings dialog", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
// Settings dialog should appear with section headings
|
||||
await expect(page.getByRole("heading", { name: "General" })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Security" })).toBeVisible();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { test as base, expect } from "@playwright/test";
|
||||
import { test as uiTest } from "./helpers";
|
||||
import { openSettings, test as uiTest } from "./helpers";
|
||||
|
||||
const API = process.env.API_URL || "http://localhost:13490";
|
||||
|
||||
@@ -323,7 +323,7 @@ base.describe("People Management — API", () => {
|
||||
|
||||
uiTest.describe("People Management — UI", () => {
|
||||
uiTest("People section displays user count and table", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
await page.getByRole("button", { name: /people/i }).click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
@@ -342,7 +342,7 @@ uiTest.describe("People Management — UI", () => {
|
||||
});
|
||||
|
||||
uiTest("Search filters users", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
await page.getByRole("button", { name: /people/i }).click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
@@ -356,7 +356,7 @@ uiTest.describe("People Management — UI", () => {
|
||||
});
|
||||
|
||||
uiTest("Add Members button is visible and interactive", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
await page.getByRole("button", { name: /people/i }).click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
@@ -380,7 +380,7 @@ uiTest.describe("People Management — UI", () => {
|
||||
uiTest(
|
||||
"Three-dot menu shows edit, reset password, and delete options",
|
||||
async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
await page.getByRole("button", { name: /people/i }).click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { test as base, expect } from "@playwright/test";
|
||||
import { login } from "./helpers";
|
||||
import { login, openSettings } from "./helpers";
|
||||
|
||||
const API = process.env.API_URL || "http://localhost:13490";
|
||||
|
||||
@@ -143,7 +143,7 @@ base.describe("RBAC Full — People Management UI", () => {
|
||||
"admin sees role dropdown with admin/editor/user options when adding members",
|
||||
async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
await page.getByRole("button", { name: /people/i }).click();
|
||||
|
||||
// Click "Add Members" to reveal the form
|
||||
@@ -174,14 +174,14 @@ base.describe("RBAC Full — Roles Management UI", () => {
|
||||
|
||||
base.test("admin sees Roles tab in settings", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
|
||||
await expect(page.getByRole("button", { name: /^roles$/i })).toBeVisible();
|
||||
});
|
||||
|
||||
base.test("roles section shows built-in roles with Built-in badge", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
await page.getByRole("button", { name: /^roles$/i }).click();
|
||||
|
||||
// Wait for roles to load
|
||||
@@ -207,14 +207,14 @@ base.describe("RBAC Full — Audit Log UI", () => {
|
||||
|
||||
base.test("admin sees Audit Log tab in settings", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
|
||||
await expect(page.getByRole("button", { name: /audit log/i })).toBeVisible();
|
||||
});
|
||||
|
||||
base.test("audit log displays LOGIN_SUCCESS entries", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
await page.getByRole("button", { name: /audit log/i }).click();
|
||||
|
||||
// Wait for audit log section to load
|
||||
@@ -244,7 +244,7 @@ base.describe("RBAC Full — API Key Scoping UI", () => {
|
||||
|
||||
base.test("API Keys section has permission scoping toggle", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
await page.getByRole("button", { name: /api keys/i }).click();
|
||||
|
||||
// The scoping toggle text should be visible
|
||||
@@ -295,7 +295,7 @@ base.describe("RBAC Full — Custom Role User", () => {
|
||||
base.test("custom role user only sees permitted tabs (no admin tabs)", async ({ page }) => {
|
||||
await login(page, CUSTOM_USER, CUSTOM_PASSWORD);
|
||||
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
|
||||
// Should see these tabs (available to all authenticated users or matching permissions)
|
||||
await expect(page.getByRole("button", { name: /general/i })).toBeVisible();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { test as base, expect } from "@playwright/test";
|
||||
import { login } from "./helpers";
|
||||
import { login, openSettings } from "./helpers";
|
||||
|
||||
const API = process.env.API_URL || "http://localhost:13490";
|
||||
|
||||
@@ -108,7 +108,7 @@ base.describe("RBAC - Admin sees all tabs", () => {
|
||||
|
||||
base.test("admin sees all settings tabs", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
|
||||
await expect(page.getByRole("button", { name: /general/i })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: /system settings/i })).toBeVisible();
|
||||
@@ -138,7 +138,7 @@ base.describe("RBAC - User sees restricted tabs", () => {
|
||||
base.test("user role only sees permitted settings tabs", async ({ page }) => {
|
||||
await login(page, TEST_USER, TEST_PASSWORD);
|
||||
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
|
||||
// Should see these tabs
|
||||
await expect(page.getByRole("button", { name: /general/i })).toBeVisible();
|
||||
@@ -251,7 +251,7 @@ base.describe("RBAC - Editor sees collaborative tabs", () => {
|
||||
"editor sees general, security, api-keys, tools, about but not admin tabs",
|
||||
async ({ page }) => {
|
||||
await login(page, "editortest", "EditorTest1");
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
|
||||
// Should see these
|
||||
await expect(page.getByRole("button", { name: /general/i })).toBeVisible();
|
||||
|
||||
+10
-10
@@ -1,15 +1,15 @@
|
||||
import { expect, test } from "./helpers";
|
||||
import { expect, openSettings, test } from "./helpers";
|
||||
|
||||
test.describe("Settings Dialog", () => {
|
||||
test("opens from sidebar", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
|
||||
// Settings dialog should appear with sections
|
||||
await expect(page.getByText("General").first()).toBeVisible();
|
||||
});
|
||||
|
||||
test("General section shows user info", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
|
||||
// Should show username and version info
|
||||
await expect(page.getByText(/admin/i).first()).toBeVisible();
|
||||
@@ -17,14 +17,14 @@ test.describe("Settings Dialog", () => {
|
||||
});
|
||||
|
||||
test("General section has logout button", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
|
||||
const logoutBtn = page.getByRole("button", { name: /logout|log out/i });
|
||||
await expect(logoutBtn).toBeVisible();
|
||||
});
|
||||
|
||||
test("Security section has change password form", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
|
||||
// Navigate to Security section
|
||||
await page.getByText("Security").click();
|
||||
@@ -34,7 +34,7 @@ test.describe("Settings Dialog", () => {
|
||||
});
|
||||
|
||||
test("People section shows user list", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
|
||||
// Navigate to People section
|
||||
await page.getByText("People").click();
|
||||
@@ -54,7 +54,7 @@ test.describe("Settings Dialog", () => {
|
||||
await page.goto("/");
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const apiKeysBtn = page.getByRole("button", { name: /api keys/i });
|
||||
@@ -70,7 +70,7 @@ test.describe("Settings Dialog", () => {
|
||||
});
|
||||
|
||||
test("About section shows app info", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
|
||||
// Navigate to About section
|
||||
await page.getByText("About").click();
|
||||
@@ -80,7 +80,7 @@ test.describe("Settings Dialog", () => {
|
||||
});
|
||||
|
||||
test("System Settings section has configuration", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
|
||||
// Navigate to System Settings section
|
||||
await page.getByText("System Settings").click();
|
||||
@@ -90,7 +90,7 @@ test.describe("Settings Dialog", () => {
|
||||
});
|
||||
|
||||
test("settings dialog can be closed", async ({ loggedInPage: page }) => {
|
||||
await page.locator("aside").getByText("Settings").click();
|
||||
await openSettings(page);
|
||||
await expect(page.getByText("General").first()).toBeVisible();
|
||||
|
||||
// Close by clicking X or outside
|
||||
|
||||
Reference in New Issue
Block a user