/** @vitest-environment jsdom */ import { act } from "react"; import { createRoot, type Root } from "react-dom/client"; import { afterEach, describe, expect, it, vi } from "vitest"; import type { GamePluginResponse } from "../api/types"; import { defaultServerCreateForm } from "../contracts/serverManagement"; import { serverCreateRequestFromForm } from "../schemas/serverManagement"; import { ServerDeploymentWorkflow } from "./ServerDeploymentWorkflow"; const plugin: GamePluginResponse = { id: "game.runtime", name: "Runtime Game", version: "1.0.0", serverType: "runtime", manifestRef: "artifact://runtime-manifest", createFormSchemaRef: "schemas/create.json", createFields: [{ key: "serverRoot", label: "服务器目录", type: "text", required: true }], requiredRunCapabilities: ["process.install"], declaredPermissions: ["server.create"], permissions: { ai: false, logs: true, files: false, jobs: true, artifacts: false }, lifecycleActions: { install: "actions/install.json", start: "actions/start.json", stop: "actions/stop.json" }, bridgeActions: [], pages: [], tags: [], aiPurposes: [], productionLifecycle: { operations: ["install"], dependencyPolicy: "optional", approvalRequired: [] }, status: "installed", runtimeProfiles: { transportProfiles: [{ key: "rcon", kind: "rcon", targetKey: "rcon.password", capabilities: ["remote.run.rcon.command"] }], lifecycleProfiles: [{ key: "local", mode: "local-process", capabilities: ["process.install"], transportKeys: ["rcon"] }] } }; let root: Root | null = null; let container: HTMLDivElement | null = null; (globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true; afterEach(async () => { if (root) { await act(async () => root?.unmount()); } container?.remove(); root = null; container = null; }); describe("ServerDeploymentWorkflow", () => { it("submits deployment mode and matching startup inputs from the create wizard", async () => { container = document.createElement("div"); document.body.append(container); root = createRoot(container); const initialForm = defaultServerCreateForm([plugin]); let submitted: ReturnType | undefined; const onSubmit = vi.fn(async (form: typeof initialForm) => { submitted = serverCreateRequestFromForm(form, 17); }); await act(async () => { root?.render( undefined} onSubmit={onSubmit} /> ); }); expect(container.querySelector('select[name="pluginId"]')).not.toBeNull(); expect(container.querySelector('input[name="name"]')).not.toBeNull(); for (const field of ["deploymentTargetId", "runEndpointId", "profileKey", "serverRoot", "startCommand", "deploymentMode"]) { expect(container.querySelector(`[name="${field}"]`)).toBeNull(); } expect(container.textContent).not.toContain("运行连接设置"); const nameInput = container.querySelector('input[name="name"]'); if (!nameInput) throw new Error("server name input not found"); await act(async () => { setInputValue(nameInput, "Custom Runtime Server"); }); await submitWorkflow(container); expect(container.textContent).toContain("选择这台服务器的创建方式"); expect(container.textContent).toContain("自定义启动方式"); await clickButtonByText(container, "自定义启动方式"); await submitWorkflow(container); expect(container.querySelector('input[name="startCommand"]')).not.toBeNull(); expect(container.querySelector('[name="deploymentTargetId"]')).toBeNull(); expect(container.querySelector('[name="runEndpointId"]')).toBeNull(); expect(container.querySelector('[name="profileKey"]')).toBeNull(); const rootInput = container.querySelector('input[name="serverRoot"]'); const startInput = container.querySelector('input[name="startCommand"]'); if (!rootInput || !startInput) throw new Error("custom startup inputs not found"); await act(async () => { setInputValue(rootInput, "/srv/custom-runtime"); setInputValue(startInput, "./start-runtime.sh"); }); await submitWorkflow(container); expect(container.textContent).toContain("本次保存创建向导配置"); expect(container.textContent).toContain("自定义启动方式"); expect(container.textContent).toContain("将替换"); await submitWorkflow(container); expect(onSubmit).toHaveBeenCalledTimes(1); expect(submitted).toMatchObject({ id: "server-custom-runtime-server-17", pluginId: "game.runtime", name: "Custom Runtime Server", idempotencyKey: "web:create:server-custom-runtime-server-17:17", deployment: { mode: "custom-command", serverRoot: "/srv/custom-runtime", startCommand: "./start-runtime.sh" } }); expect(submitted && "runEndpointId" in submitted).toBe(false); expect(submitted && "profileKey" in submitted).toBe(false); expect(submitted && "bindings" in submitted).toBe(false); expect(submitted?.deployment && "profileKey" in submitted.deployment).toBe(false); expect(submitted?.deployment && "runtimeBindings" in submitted.deployment).toBe(false); }); }); async function submitWorkflow(target: HTMLElement) { const form = target.querySelector('form[aria-label="创建服务器部署向导"]'); if (!form) throw new Error("create workflow form not found"); await act(async () => { form.dispatchEvent(new SubmitEvent("submit", { bubbles: true, cancelable: true })); }); } async function clickButtonByText(target: HTMLElement, text: string) { const button = Array.from(target.querySelectorAll("button")).find((item) => item.textContent?.includes(text)); if (!button) throw new Error(`button not found: ${text}`); await act(async () => { button.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true })); }); } function setInputValue(input: HTMLInputElement, value: string) { const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set; setter?.call(input, value); input.dispatchEvent(new Event("input", { bubbles: true })); }