Files
browser/platform_web/components/ServerConfigEditor.test.tsx
T

144 lines
7.7 KiB
TypeScript

/** @vitest-environment jsdom */
import { act, useState } from "react";
import { createRoot, type Root } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { ServerFileWorkspaceResponse, ServerInstanceResponse } from "../api/types";
import type { OperationTracker } from "../stores/operations";
import { ServerConfigEditor } from "./ServerConfigEditor";
const apiMocks = vi.hoisted(() => ({
getServerFileReadSnapshot: vi.fn(),
getServerFileWorkspace: vi.fn(),
readServerFile: vi.fn()
}));
vi.mock("../api/client", () => ({ platformApiClient: apiMocks }));
const instance = { id: "server-1", name: "SCUM Alpha" } as ServerInstanceResponse;
const workspace = {
serverInstanceId: "server-1",
pluginId: "game.scum",
defaultDirectoryKey: "scum-config",
directories: [{ key: "scum-config", label: "服务器配置", scope: "config" }],
files: [{ key: "scum-server-settings", directoryKey: "scum-config", label: "ServerSettings.ini", kind: "config", editable: true }],
configFields: [],
transfer: { channel: "run-file-transfer", uploadChunkSizeBytes: 1, downloadChunkSizeBytes: 1, maxInlineEditBytes: 64, maxBrowserUploadBytes: 64, notes: [] },
declaredOnly: true
} as ServerFileWorkspaceResponse;
let root: Root | null = null;
let container: HTMLDivElement | null = null;
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
beforeEach(() => {
vi.useFakeTimers();
apiMocks.getServerFileWorkspace.mockResolvedValue(workspace);
let snapshotCalls = 0;
apiMocks.getServerFileReadSnapshot.mockImplementation(async () => {
snapshotCalls += 1;
return snapshotCalls === 1 ? { serverInstanceId: "server-1", pluginId: "game.scum", key: "scum-server-settings", state: "not-read" } : { serverInstanceId: "server-1", pluginId: "game.scum", key: "scum-server-settings", state: "ready", content: "[General]\nscum.MaxPlayers=63\n", version: 1, checksum: "sha256:test" };
});
apiMocks.readServerFile.mockResolvedValue({ status: "queued", serverInstanceId: "server-1", pluginId: "game.scum", operation: "read", key: "scum-server-settings", job: { id: "job-read", state: "queued" } });
});
afterEach(async () => {
if (root) await act(async () => root?.unmount());
container?.remove();
root = null;
container = null;
vi.clearAllMocks();
vi.useRealTimers();
});
describe("ServerConfigEditor", () => {
it("does not restart the same snapshot request when operation state rerenders the parent", async () => {
container = document.createElement("div");
document.body.append(container);
root = createRoot(container);
function Wrapper() {
const [, rerender] = useState(0);
const operations: OperationTracker = {
operations: [],
begin: () => { rerender((value) => value + 1); return "operation-read"; },
update: () => undefined,
succeed: () => undefined,
fail: () => undefined,
isPending: () => false
};
return <ServerConfigEditor instance={instance} operations={operations} requester="Operator" onClose={() => undefined} />;
}
await act(async () => root?.render(<Wrapper />));
await act(async () => { await Promise.resolve(); });
await act(async () => { await vi.advanceTimersByTimeAsync(500); });
await act(async () => { await Promise.resolve(); });
expect(apiMocks.readServerFile).toHaveBeenCalledTimes(1);
expect(apiMocks.getServerFileReadSnapshot).toHaveBeenCalledTimes(2);
});
it("surfaces a terminal Run read failure without polling for thirty seconds", async () => {
apiMocks.getServerFileReadSnapshot.mockResolvedValue({ serverInstanceId: "server-1", pluginId: "game.scum", key: "scum-server-settings", state: "failed", reason: "Run 文件读取失败:file is missing" });
container = document.createElement("div");
document.body.append(container);
root = createRoot(container);
const operations: OperationTracker = { operations: [], begin: () => "operation-read", update: () => undefined, succeed: () => undefined, fail: () => undefined, isPending: () => false };
await act(async () => root?.render(<ServerConfigEditor instance={instance} operations={operations} requester="Operator" onClose={() => undefined} />));
await act(async () => { await Promise.resolve(); await Promise.resolve(); });
expect(apiMocks.readServerFile).not.toHaveBeenCalled();
expect(apiMocks.getServerFileReadSnapshot).toHaveBeenCalledTimes(1);
expect(container.textContent).toContain("file is missing");
expect(container.textContent).toContain("重试");
});
it("does not dispatch a second read while an existing Run read is pending", async () => {
apiMocks.getServerFileReadSnapshot.mockResolvedValue({ serverInstanceId: "server-1", pluginId: "game.scum", key: "scum-server-settings", state: "pending", reason: "等待运行端完成文件读取。" });
container = document.createElement("div");
document.body.append(container);
root = createRoot(container);
const operations: OperationTracker = { operations: [], begin: () => "operation-read", update: () => undefined, succeed: () => undefined, fail: () => undefined, isPending: () => false };
await act(async () => root?.render(<ServerConfigEditor instance={instance} operations={operations} requester="Operator" onClose={() => undefined} />));
await act(async () => { await Promise.resolve(); await Promise.resolve(); });
expect(apiMocks.readServerFile).not.toHaveBeenCalled();
expect(apiMocks.getServerFileReadSnapshot).toHaveBeenCalledTimes(1);
await act(async () => { await vi.advanceTimersByTimeAsync(500); });
expect(apiMocks.readServerFile).not.toHaveBeenCalled();
expect(apiMocks.getServerFileReadSnapshot).toHaveBeenCalledTimes(2);
});
it("retries a failed read with a new idempotency key and loads the file", async () => {
let retryRequested = false;
apiMocks.getServerFileReadSnapshot.mockImplementation(async () => retryRequested
? { serverInstanceId: "server-1", pluginId: "game.scum", key: "scum-server-settings", state: "ready", content: "[General]\nscum.MaxPlayers=63\n", version: 1, checksum: "sha256:test" }
: { serverInstanceId: "server-1", pluginId: "game.scum", key: "scum-server-settings", state: "failed", reason: "Run 文件读取失败:file is missing" });
apiMocks.readServerFile.mockImplementation(async (_serverId: string, request: { idempotencyKey: string }) => {
retryRequested = true;
return { status: "queued", serverInstanceId: "server-1", pluginId: "game.scum", operation: "read", key: "scum-server-settings", job: { id: "job-read-retry", state: "queued" }, request };
});
container = document.createElement("div");
document.body.append(container);
root = createRoot(container);
const operations: OperationTracker = { operations: [], begin: () => "operation-read", update: () => undefined, succeed: () => undefined, fail: () => undefined, isPending: () => false };
await act(async () => root?.render(<ServerConfigEditor instance={instance} operations={operations} requester="Operator" onClose={() => undefined} />));
await act(async () => { await Promise.resolve(); await Promise.resolve(); });
const retry = [...(container.querySelectorAll("button"))].find((button) => button.textContent?.includes("重试"));
expect(retry).toBeDefined();
await act(async () => retry?.click());
await act(async () => { await vi.advanceTimersByTimeAsync(500); await Promise.resolve(); });
expect(apiMocks.readServerFile).toHaveBeenCalledTimes(1);
expect(apiMocks.readServerFile.mock.calls[0][1].idempotencyKey).toBe("web:server-config:read:server-1:scum-server-settings:1");
expect(container.textContent).toContain("已读取 ServerSettings.ini");
});
});