/** @vitest-environment jsdom */ import { act } from "react"; import { createRoot, type Root } from "react-dom/client"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { ServerInstanceResponse } from "../api/types"; import type { PageComponentProps } from "../contracts/page"; import { capabilitiesForRoles, type CurrentUserView } from "../contracts/workspace"; import type { OperationTracker } from "../stores/operations"; import { ServersPage } from "./ServersPage"; const apiMocks = vi.hoisted(() => ({ listGamePlugins: vi.fn(), listRunEndpoints: vi.fn(), listServerInstances: vi.fn(), listJobs: vi.fn(), listServerMetrics: vi.fn(), deleteServerInstance: vi.fn() })); vi.mock("../api/client", () => ({ platformApiClient: apiMocks })); const adminUser: CurrentUserView = { id: "user-admin", displayName: "Operator", status: "active", roles: ["platformAdmin"], capabilities: capabilitiesForRoles(["platformAdmin"]), profile: {}, source: "local" }; const noopOperations = { operations: [], begin: () => "operation-1", update: () => undefined, succeed: () => undefined, fail: () => undefined, isPending: () => false } as unknown as OperationTracker; const instance = { id: "server-1", name: "SCUM Alpha", pluginId: "server.scum", pluginVersion: "1.0.0", runEndpointId: "run-local", ownerUserId: "user-admin", state: "ready", configVersion: 1 } as ServerInstanceResponse; let container: HTMLDivElement | null = null; let root: Root | null = null; (globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true; beforeEach(() => { apiMocks.listGamePlugins.mockResolvedValue({ items: [], count: 0 }); apiMocks.listRunEndpoints.mockResolvedValue({ items: [], count: 0 }); apiMocks.listServerInstances.mockResolvedValue({ items: [instance], count: 1 }); apiMocks.listJobs.mockResolvedValue({ items: [], count: 0 }); apiMocks.listServerMetrics.mockResolvedValue({ items: [], count: 0 }); container = document.createElement("div"); document.body.appendChild(container); }); afterEach(async () => { await act(async () => { root?.unmount(); }); root = null; container?.remove(); container = null; document.body.innerHTML = ""; vi.clearAllMocks(); }); function pageProps(): PageComponentProps { return { session: adminUser, params: {}, operations: noopOperations, onNavigate: () => undefined, onLogout: async () => undefined, onProfileSave: async () => adminUser, onThemePreferenceSave: async () => ({ userId: adminUser.id, paletteId: "mecha-black", backgroundPresetId: "mecha-grid", persistence: "api", updatedAt: "2026-09-15T00:00:00Z" }) }; } async function renderServersPage(): Promise { await act(async () => { root = createRoot(container!); root.render(); }); await act(async () => { await Promise.resolve(); }); } function searchInput(): HTMLInputElement | null { return container!.querySelector(".server-toolbar input[type='search']"); } function findButton(text: string, scope: ParentNode = document.body): HTMLButtonElement { const button = Array.from(scope.querySelectorAll("button")).find((candidate) => candidate.textContent?.includes(text)); if (!button) throw new Error(`button not found: ${text}`); return button; } describe("ServersPage server search isolation", () => { it("keeps the search field out of the DOM while the delete password prompt is open", async () => { await renderServersPage(); const input = searchInput(); expect(input).not.toBeNull(); expect(input!.getAttribute("autocomplete")).toBe("new-password"); expect(input!.getAttribute("data-1p-ignore")).toBe("true"); expect(input!.getAttribute("data-lpignore")).toBe("true"); expect(input!.getAttribute("data-bwignore")).toBe("true"); expect(input!.getAttribute("data-protonpass-ignore")).toBe("true"); const menuButton = container!.querySelector("button[aria-haspopup='menu']"); expect(menuButton).not.toBeNull(); await act(async () => { menuButton!.click(); }); await act(async () => { findButton("删除服务器").click(); }); expect(document.querySelector(".confirm-panel[role='dialog']")).not.toBeNull(); expect(document.querySelector("input[name='server-delete-password-confirmation']")).not.toBeNull(); expect(searchInput()).toBeNull(); expect(container!.querySelector(".server-search-suspended")?.textContent).toContain("搜索暂时停用"); await act(async () => { findButton("取消", document.querySelector(".confirm-panel") ?? document.body).click(); }); expect(document.querySelector(".confirm-panel[role='dialog']")).toBeNull(); expect(searchInput()).not.toBeNull(); expect(apiMocks.deleteServerInstance).not.toHaveBeenCalled(); }); });