Files
browser/platform_web/pages/ConsolePages.test.tsx
T

424 lines
21 KiB
TypeScript

import { renderToStaticMarkup } from "react-dom/server";
import { describe, expect, it } from "vitest";
import { HomePage } from "./HomePage";
import { MaintenancePage } from "./MaintenancePage";
import { PluginsPage } from "./PluginsPage";
import { ProfileSettingsPage } from "./ProfileSettingsPage";
import { ServerDetailPage } from "./ServerDetailPage";
import { ServersPage } from "./ServersPage";
import { UsersPage } from "./UsersPage";
import runtimeTaskProgressSource from "../components/RuntimeTaskProgress.tsx?raw";
import serverDeploymentWorkflowSource from "../components/ServerDeploymentWorkflow.tsx?raw";
import serverLiveOperationsSource from "../components/ServerLiveOperations.tsx?raw";
import serversPageSource from "./ServersPage.tsx?raw";
import serverDetailPageSource from "./ServerDetailPage.tsx?raw";
import type { PageComponentProps } from "../contracts/page";
import { capabilitiesForRoles, type CurrentUserView } from "../contracts/workspace";
import type { OperationTracker } from "../stores/operations";
import type { UserResponse } from "../api/types";
const adminUser: CurrentUserView = {
id: "user-admin",
displayName: "Operator",
status: "active",
roles: ["platformAdmin"],
capabilities: capabilitiesForRoles(["platformAdmin"]),
profile: {},
source: "local"
};
const noopOperations: OperationTracker = {
operations: [],
begin: () => "op-test",
update: () => undefined,
succeed: () => undefined,
fail: () => undefined,
isPending: () => false
};
const reviewUser: UserResponse = {
id: "user-reviewer",
displayName: "Plugin Reviewer",
email: "reviewer@example.test",
status: "pending",
roles: ["server-admin"],
profile: { contactNote: "needs approval" },
createdAt: "2026-07-03T00:00:00Z",
updatedAt: "2026-07-03T00:00:00Z"
};
function pageProps(params: PageComponentProps["params"] = {}): 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-07-03T00:00:00Z" })
};
}
describe("first-party console pages", () => {
it("renders the platform overview with first-screen health modules", () => {
const html = renderToStaticMarkup(<HomePage {...pageProps()} />);
expect(html).toContain("平台概览");
expect(html).toContain("资源负载");
expect(html).toContain("游戏类型分布");
expect(html).toContain("最近运营信号");
expect(html).not.toContain("sk-");
});
it("keeps unavailable overview modules visible instead of claiming an empty healthy state", () => {
const html = renderToStaticMarkup(
<HomePage
{...pageProps()}
initialState={{
core: {
status: "ready",
refreshedAt: "2026-07-18T10:00:00Z",
data: {
instances: [],
endpoints: [],
jobs: [
{
id: "job-failed-1",
serverInstanceId: "server-1",
runEndpointId: "endpoint-1",
capability: "server.lifecycle.start",
idempotencyKey: "idem-1",
state: "failed",
progress: { percent: 42 },
retryPolicy: { maxAttempts: 3, initialBackoffSeconds: 1, maxBackoffSeconds: 10 },
attempt: 3,
reconcileCount: 0,
createdAt: "2026-07-18T09:59:00Z",
updatedAt: "2026-07-18T10:00:00Z"
}
]
}
},
metrics: { status: "error", reason: "metrics unavailable", diagnosticId: "metrics-1" },
usage: { status: "error", reason: "usage unavailable", diagnosticId: "usage-1" },
providers: { status: "error", reason: "providers unavailable", diagnosticId: "providers-1" },
signals: { status: "error", reason: "audit unavailable", diagnosticId: "audit-1" }
}}
/>
);
expect(html).toContain("4 个模块不可用");
expect(html).toContain("资源指标不可用");
expect(html).toContain("审计信号不可用");
expect(html).toContain("AI 提供商信号不可用");
expect(html).toContain("server.lifecycle.start");
expect(html).not.toContain("暂无异常信号");
});
it("explains read-only overview access when the session cannot create servers", () => {
const readOnlySession: CurrentUserView = {
...adminUser,
roles: ["serverAdmin"],
capabilities: ["platform.overview.read", "servers.read"]
};
const html = renderToStaticMarkup(
<HomePage
{...pageProps()}
session={readOnlySession}
initialState={{
core: { status: "ready", data: { instances: [], endpoints: [], jobs: [] }, refreshedAt: "2026-07-18T10:00:00Z" },
metrics: { status: "ready", data: [], refreshedAt: "2026-07-18T10:00:00Z" },
usage: { status: "error", reason: "unavailable", diagnosticId: "usage" },
providers: { status: "ready", data: [], refreshedAt: "2026-07-18T10:00:00Z" },
signals: { status: "ready", data: [], refreshedAt: "2026-07-18T10:00:00Z" }
}}
/>
);
expect(html).toContain("没有创建服务器的权限");
expect(html).toContain("前往服务器管理");
});
it("renders the server list workspace with search and status filters", () => {
const html = renderToStaticMarkup(<ServersPage {...pageProps()} />);
expect(html).toContain("服务器管理");
expect(html).toContain("搜索服务器");
expect(html).toContain("autoComplete=\"off\"");
expect(html).toContain("data-1p-ignore=\"true\"");
expect(html).toContain("data-lpignore=\"true\"");
expect(html).toContain("在线");
expect(html).toContain("离线");
expect(html).not.toContain("run socket");
expect(html).not.toContain("/Users/");
});
it("keeps server metrics unavailable and runtime commands permission-gated", () => {
expect(serversPageSource).toContain("metricsUnavailable");
expect(serversPageSource).toContain("指标不可用");
expect(serversPageSource).toContain("canManageServers");
expect(serversPageSource).toContain("当前账号没有运行操作权限");
expect(serversPageSource).toContain("failedJobs");
expect(serversPageSource).toContain("refreshMetrics");
expect(serversPageSource).toContain("onClick={() => void refreshMetrics()}");
});
it("exposes run key reset from the compact server list danger menu", () => {
expect(serversPageSource).toContain("reset-run-key");
expect(serversPageSource).toContain("重置 run 密钥");
expect(serversPageSource).toContain("runtimeKeyResetStages");
expect(serversPageSource).toContain("platformApiClient.resetRunKey");
expect(serversPageSource).toContain("旧 run 会话已失效");
});
it("keeps deployment startup inputs in the create wizard without exposing protected values", () => {
expect(serversPageSource).toContain("<ServerDeploymentWorkflow");
expect(serversPageSource).toContain("openEditDeployment");
expect(serversPageSource).toContain("编辑部署");
expect(serversPageSource).not.toContain('instance.state === "running" || instance.state === "installing"');
expect(serverDetailPageSource).not.toContain("<ServerDeploymentWorkflow");
expect(serverDetailPageSource).toContain("ServerDeploymentSection");
expect(serverDeploymentWorkflowSource).toContain("基本信息");
expect(serverDeploymentWorkflowSource).toContain("部署方式");
expect(serverDeploymentWorkflowSource).toContain("相关配置");
expect(serverDeploymentWorkflowSource).toContain("专属 Run");
expect(serverDeploymentWorkflowSource).toContain("创建服务器");
expect(serverDeploymentWorkflowSource).toContain("执行目录(可选)");
expect(serverDeploymentWorkflowSource).toContain("默认使用服务器目录");
expect(serverDeploymentWorkflowSource).toContain("安装目录{isScum ? \"(必填)\" : \"(可选)\"}");
expect(serverDeploymentWorkflowSource).toContain("新建并安装执行流程");
expect(serverDeploymentWorkflowSource).toContain("安装目录”就是游戏服务端、数据和配置将落地的位置");
expect(serverDeploymentWorkflowSource).toContain("通过 SteamCMD 安装 App 3792580 到该目录");
expect(serverDeploymentWorkflowSource).toContain("全部 4 步通过才算安装成功");
expect(serverDeploymentWorkflowSource).toContain("已有服务器目录");
expect(serverDeploymentWorkflowSource).toContain("不会重装或覆盖现有游戏配置");
expect(serverDeploymentWorkflowSource).toContain("接管已有服务器执行流程");
expect(serverDeploymentWorkflowSource).toContain("不需要填写 SteamCMD 目录");
expect(serverDeploymentWorkflowSource).toContain("当前平台尚未提供 SCUM 服务端的受控升级任务");
expect(serverDeploymentWorkflowSource).toContain("已绑定服务器编辑时会直接进入相关配置");
expect(serverDeploymentWorkflowSource).toContain("可在此调整部署方式;不会重复要求选择已绑定的运行节点");
expect(serversPageSource).toContain('onNavigate("serverDetail", { serverId: result.instance.id, routeKey: "run-builder" })');
expect(serverDetailPageSource).toContain("运行配置绑定");
expect(serverDetailPageSource).toContain('type={field.sensitive ? "password" : "text"}');
expect(serverDeploymentWorkflowSource).toContain("显示已保存配置");
expect(serverDeploymentWorkflowSource).toContain("revealSavedInputs");
expect(serversPageSource).toContain("revealServerDeployment");
expect(serverDetailPageSource).toContain("最近 Run 调度");
expect(serversPageSource).toContain("serverCreateRequestFromForm(nextForm)");
expect(serverDeploymentWorkflowSource).not.toContain('name="id"');
expect(serverDeploymentWorkflowSource).not.toContain("实例 ID");
for (const forbidden of ["secret://", "/Users/", "/var/run/", "unix://", "tcp://"]) {
expect(serverDeploymentWorkflowSource).not.toContain(forbidden);
}
});
it("keeps live logs and management terminal on server detail instead of the server list", () => {
expect(serversPageSource).not.toContain("ServerLiveLogDrawer");
expect(serversPageSource).not.toContain("ServerManagementTerminalDrawer");
expect(serversPageSource).not.toContain("live-logs");
expect(serversPageSource).not.toContain("管理终端");
expect(serverDetailPageSource).toContain("ServerLiveLogDrawer");
expect(serverDetailPageSource).toContain("ServerManagementTerminalDrawer");
expect(serverDetailPageSource).toContain("实时日志");
expect(serverDetailPageSource).toContain("管理终端");
});
it("keeps create target and profile controls out while preserving deployment steps", () => {
expect(serverDeploymentWorkflowSource).not.toContain('name="deploymentTargetId"');
expect(serverDeploymentWorkflowSource).not.toContain("请选择部署目标");
expect(serverDeploymentWorkflowSource).not.toContain("saveAsDraft");
expect(serverDeploymentWorkflowSource).not.toContain("暂不指定部署目标");
expect(serverDeploymentWorkflowSource).toContain('if (step === pluginStep) return Boolean(form.pluginId) && Boolean(form.name.trim());');
expect(serverDeploymentWorkflowSource).toContain("await onSubmit(form)");
expect(serverDeploymentWorkflowSource).not.toContain('kind === "create" && <label>运行预设');
expect(serverDeploymentWorkflowSource).not.toContain('kind === "create" && bindingFields');
expect(serverDeploymentWorkflowSource).toContain("配置启动项");
expect(serverDeploymentWorkflowSource).toContain("本次保存创建向导配置");
expect(serverDeploymentWorkflowSource).toContain('const modeStep = kind === "create" ? 1 : -1;');
expect(serverDeploymentWorkflowSource).toContain('const configurationStep = kind === "create" ? 2 : needsTargetSelection ? 1 : 0;');
expect(serverDeploymentWorkflowSource).toContain('const needsTargetSelection = kind === "edit" && !initialForm.runEndpointId;');
expect(serversPageSource).toContain("serverCreateRequestFromForm(nextForm)");
});
it("renders server runtime actions as a compact popover trigger instead of an in-card details stack", () => {
expect(serversPageSource).toContain('aria-haspopup="menu"');
expect(serversPageSource).toContain("createPortal");
expect(serversPageSource).toContain("runtime-action-popover");
expect(serversPageSource).not.toContain("runtime-action-menu");
expect(serversPageSource).not.toContain("<details");
});
it("places server deletion in the list runtime actions with password confirmation", () => {
const deleteHandlerSource = serversPageSource.split("async function handleDeleteServer")[1]?.split("function openRunTargetSelection")[0] ?? "";
expect(deleteHandlerSource).toContain("deleteServerInstance(deleteConfirmation.serverInstanceId, {");
expect(deleteHandlerSource).toContain("password: deletePassword");
expect(serversPageSource).toContain("serverDeleteConfirmation(card.instance)");
expect(serversPageSource).toContain("serverDeleteDisabledReason(session, card.instance)");
expect(serversPageSource).toContain("危险操作");
expect(serversPageSource).toContain("删除服务器");
expect(serversPageSource).toContain("请输入当前登录密码");
expect(serversPageSource).toContain("name=\"server-delete-password-confirmation\"");
expect(serversPageSource).toContain("autoComplete=\"new-password\"");
expect(serversPageSource).not.toContain("autoComplete=\"current-password\"");
expect(serversPageSource).toContain("canOpenActions");
expect(deleteHandlerSource).toContain("await refresh();");
expect(deleteHandlerSource).not.toContain("onNavigate");
expect(serverDetailPageSource).not.toContain("deleteServerInstance");
expect(serverDetailPageSource).not.toContain("serverDeleteConfirmation");
});
it("surfaces runtime actions through progress dialogs with build stages", () => {
const generateRunSource = serversPageSource.split("async function generateRunForTarget")[1]?.split("async function handleQuickRuntimeAction")[0] ?? "";
expect(serversPageSource).toContain("RuntimeTaskProgressDialog");
expect(serversPageSource).toContain("选择生成平台");
expect(serversPageSource).toContain("runTargetSelection");
expect(serversPageSource).toContain("runtimeRunBuildStages");
expect(serversPageSource).toContain("requireQuickRuntimeActionAvailable(instance.id, action)");
expect(serversPageSource).toContain("该操作不可用");
expect(generateRunSource.indexOf("runtimeTask.runTrackedTask")).toBeLessThan(generateRunSource.indexOf('await requireQuickRuntimeActionAvailable(instance.id, "generate-run")'));
expect(serverDetailPageSource).toContain("RuntimeTaskProgressDialog");
expect(serverDetailPageSource).toContain("runtimeRunBuildStages");
expect(runtimeTaskProgressSource).toContain("runtimeBuildStages");
expect(runtimeTaskProgressSource).toContain("runtimeRunBuildStages");
expect(runtimeTaskProgressSource).toContain("进度展示");
expect(runtimeTaskProgressSource).toContain("activeStepLabel");
expect(runtimeTaskProgressSource).toContain("平台正在执行当前阶段");
expect(runtimeTaskProgressSource).toContain("拉取 run 更新");
expect(runtimeTaskProgressSource).toContain("检测构建环境");
expect(runtimeTaskProgressSource).toContain("构建中");
expect(runtimeTaskProgressSource).toContain("构建完成");
expect(runtimeTaskProgressSource).toContain("拉取代码");
expect(runtimeTaskProgressSource).toContain("安装环境");
expect(runtimeTaskProgressSource).toContain("编译构建");
expect(runtimeTaskProgressSource).toContain("打包成功");
expect(runtimeTaskProgressSource).toContain("构建成功");
});
it("renders server detail sections for daily operations", () => {
const html = renderToStaticMarkup(<ServerDetailPage {...pageProps({ serverId: "server-example-1" })} />);
expect(html).toContain("返回列表");
expect(html).not.toContain("概览");
expect(html).toContain("日志");
expect(html).toContain("管理终端");
expect(html).toContain("运行操作");
expect(html).toContain("配置");
expect(html).toContain("插件控制");
expect(html).toContain("AI 助手");
expect(html).toContain("操作历史");
});
it("renders management terminal as a large command console with quick commands", () => {
expect(serverLiveOperationsSource).toContain("management-terminal-drawer");
expect(serverLiveOperationsSource).toContain("terminal-output-topbar");
expect(serverLiveOperationsSource).toContain("terminal-command-dock");
expect(serverLiveOperationsSource).toContain("terminalQuickCommandCatalog");
expect(serverLiveOperationsSource).toContain("terminalQuickCommandsForPlugin");
expect(serverLiveOperationsSource).toContain("queueGameClientBridgeCommand");
expect(serverLiveOperationsSource).toContain("scumManagementRCONCommandRequest");
expect(serverLiveOperationsSource).toContain("#ListSquads");
expect(serverLiveOperationsSource).toContain("#ListSpawnedVehicles");
expect(serverLiveOperationsSource).toContain("selectQuickCommand(item)");
expect(serverLiveOperationsSource).toContain("hideHeader");
expect(serverLiveOperationsSource).toContain("handleCommandKeyDown");
expect(serverLiveOperationsSource).toContain("commandHistory");
expect(serverLiveOperationsSource).toContain("ArrowUp");
expect(serverLiveOperationsSource).toContain("listServerLiveLogs");
expect(serverLiveOperationsSource).toContain("openServerLogEvents");
expect(serverLiveOperationsSource).toContain("getGameClientBridgeCommand");
expect(serverLiveOperationsSource).toContain("terminalLineFromBridgeCommand");
expect(serverLiveOperationsSource).toContain("streams.filter((stream) => stream.latestSeq > 0)");
expect(serverLiveOperationsSource).toContain("SSE 实时推送");
expect(serverLiveOperationsSource).toContain("mergeTerminalLines");
expect(serverLiveOperationsSource).not.toContain("terminalLogPollMs = 1000");
expect(serverLiveOperationsSource).not.toContain("logStreamPollMs = 5000");
expect(serverLiveOperationsSource).not.toContain("queryLogStream(");
expect(serverLiveOperationsSource).not.toContain("SaveWorld");
});
it("uses declared SCUM log source keys for log backfill defaults", () => {
expect(serversPageSource).toContain("scum-server-events");
expect(serverDetailPageSource).toContain("scum-server-events");
expect(serversPageSource).not.toContain("server-log");
expect(serverDetailPageSource).not.toContain("server-log");
});
it("renders plugin catalog bridge readiness", () => {
const html = renderToStaticMarkup(<PluginsPage />);
expect(html).toContain("插件市场");
expect(html).toContain("平台 API");
expect(html).toContain("正在加载插件市场");
expect(html).not.toContain("billing");
});
it("renders user access review context", () => {
const html = renderToStaticMarkup(<UsersPage {...pageProps()} initialState={{ users: [reviewUser], loading: false, source: "local-development" }} />);
expect(html).toContain("用户管理");
expect(html).toContain("Plugin Reviewer");
expect(html).toContain("needs approval");
expect(html).toContain("本地开发样例 / 仅查看");
});
it("renders maintenance triage entry points", () => {
const html = renderToStaticMarkup(<MaintenancePage {...pageProps()} />);
expect(html).toContain("系统维护");
expect(html).toContain("维护排障入口");
expect(html).toContain("节点详情");
expect(html).toContain("最近失败任务");
expect(html).toContain("审计异常");
});
it("renders profile settings as a full page", () => {
const html = renderToStaticMarkup(<ProfileSettingsPage {...pageProps()} />);
expect(html).toContain("个人设置");
expect(html).toContain("个人资料");
expect(html).toContain("界面偏好");
expect(html).not.toContain("role=\"dialog\"");
});
it("keeps profile background controls focused on custom uploads", () => {
const originalWindow = globalThis.window;
Object.defineProperty(globalThis, "window", {
configurable: true,
value: {
localStorage: {
getItem: (key: string) => {
if (key === "platform-web.theme.palette") {
return "magical-girl";
}
if (key === "platform-web.theme.backgroundPreset") {
return "mecha-grid";
}
if (key === "platform-web.theme.background") {
return "data:image/png;base64,custom";
}
return null;
}
}
}
});
try {
const html = renderToStaticMarkup(<ProfileSettingsPage {...pageProps()} />);
expect(html).toContain("自定义背景");
expect(html).toContain("上传背景");
expect(html).toContain("移除背景");
expect(html).toContain("当前显示自定义上传背景;移除后回到主题配色背景。");
expect(html).not.toContain("机甲格纳库");
expect(html).not.toContain("粉月魔法阵");
expect(html).not.toContain("备用");
} finally {
Object.defineProperty(globalThis, "window", {
configurable: true,
value: originalWindow
});
}
});
});