Files
browser/platform_web/components/ServerDeploymentWorkflow.test.tsx
T
2026-08-20 17:58:36 +08:00

153 lines
6.3 KiB
TypeScript

/** @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<typeof serverCreateRequestFromForm> | undefined;
const onSubmit = vi.fn(async (form: typeof initialForm) => {
submitted = serverCreateRequestFromForm(form, 17);
});
await act(async () => {
root?.render(
<ServerDeploymentWorkflow
open
kind="create"
plugins={[plugin]}
initialForm={initialForm}
onClose={() => 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<HTMLInputElement>('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<HTMLInputElement>('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<HTMLInputElement>('input[name="serverRoot"]');
const startInput = container.querySelector<HTMLInputElement>('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<HTMLFormElement>('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<HTMLButtonElement>("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 }));
}