返回 DeepSeek-Reasonix
remote-server-panel.test.tsx
根目录 / desktop / frontend / src / __tests__ / remote-server-panel.test.tsx
1 // Run: node --import ./scripts/svg-stub-register.mjs --import tsx src/__tests__/remote-server-panel.test.tsx
2 //
3 // Tests the Remote SSH Server tab of the right-dock panel: the single
4 // "Open Remote Web" entry, Serve progress states, the workspace home-directory
5 // fallback, and the fixed remote-provider hint.
6
7 import { JSDOM } from "jsdom";
8 import React from "react";
9
10 let passed = 0;
11 let failed = 0;
12 function ok(value: boolean, label: string) {
13 if (value) {
14 process.stdout.write(` PASS ${label}\n`);
15 passed += 1;
16 } else {
17 process.stdout.write(` FAIL ${label}\n`);
18 failed += 1;
19 }
20 }
21
22 console.log("\nRemote SSH Server tab (Open Remote Web)");
23 const dom = new JSDOM("<!doctype html><html><body><div id=\"root\"></div></body></html>", {
24 pretendToBeVisual: true,
25 url: "http://localhost/",
26 });
27 (globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
28 globalThis.window = dom.window as unknown as Window & typeof globalThis;
29 globalThis.document = dom.window.document;
30 Object.defineProperty(globalThis, "navigator", { configurable: true, value: dom.window.navigator });
31 globalThis.HTMLElement = dom.window.HTMLElement;
32 globalThis.Event = dom.window.Event;
33 globalThis.KeyboardEvent = dom.window.KeyboardEvent;
34 Object.defineProperty(dom.window.HTMLElement.prototype, "attachEvent", { configurable: true, value: () => {} });
35 Object.defineProperty(dom.window.HTMLElement.prototype, "detachEvent", { configurable: true, value: () => {} });
36
37 const [{ createRoot }, { RemotePanel }, { LocaleProvider }, { useRemoteStore }, { __emitMockRemote, onRemoteServer, onRemoteStatus }] = await Promise.all([
38 import("react-dom/client"),
39 import("../components/RemotePanel"),
40 import("../lib/i18n"),
41 import("../store/remote"),
42 import("../lib/bridge"),
43 ]);
44
45 // The production subscription lives in App.tsx; wire the store to the mock
46 // event fan-out here so remote:server progress reaches the panel.
47 onRemoteServer((s) => useRemoteStore.getState().setServer(s));
48 onRemoteStatus((s) => useRemoteStore.getState().applyStatus(s));
49
50 const openCalls: Array<{ hostId: string; workspace: string }> = [];
51 window.go = { main: { App: {
52 async RemoteLastWorkspace(hostId: string) {
53 return hostId === "box" ? "/srv/app" : "";
54 },
55 async RemoteServerStatus() {
56 return { hostId: "box", workspace: "/srv/app", state: "stopped" };
57 },
58 async RemoteServerLogs() {
59 return "";
60 },
61 async OpenRemoteWorkspace(hostId: string, workspace: string) {
62 openCalls.push({ hostId, workspace });
63 },
64 async StopRemoteServer() {},
65 } } };
66
67 const host = { id: "box", label: "box", host: "box.test", port: 22, user: "dev", identityFile: "", proxyJump: "", defaultWorkspace: "/srv/app", serveInstall: "auto", useSSHConfig: false };
68 useRemoteStore.getState().setHosts([host]);
69 useRemoteStore.getState().openExplorer("box");
70 useRemoteStore.getState().setExplorerTab("server");
71 useRemoteStore.getState().applyStatus({ hostId: "box", state: "connected" });
72
73 const rootElement = document.getElementById("root");
74 if (!rootElement) throw new Error("missing root");
75 const { act } = await import("react");
76 const root = createRoot(rootElement);
77
78 await act(async () => {
79 root.render(
80 <LocaleProvider>
81 <RemotePanel onClose={() => {}} />
82 </LocaleProvider>,
83 );
84 await Promise.resolve();
85 });
86
87 ok(document.body.textContent?.includes("Open Remote Web") === true, "server tab shows the unified Open Remote Web entry");
88 ok(
89 document.body.textContent?.includes("Models, API keys, and sessions are managed by the Reasonix configuration on the remote server.") === true,
90 "server tab states that providers, API keys, and sessions are managed by the remote Reasonix configuration",
91 );
92
93 // Serve progress: remote:server events drive the busy state and label.
94 await act(async () => {
95 __emitMockRemote("server", { hostId: "box", workspace: "/srv/app", state: "starting", message: "detecting platform" });
96 await Promise.resolve();
97 });
98 ok(document.body.textContent?.includes("Starting") === true, "Serve progress renders while starting");
99 const openButton = () => Array.from(document.querySelectorAll("button")).find((b) => b.textContent?.includes("Open Remote Web"));
100 ok(openButton()?.hasAttribute("disabled") === true, "Open Remote Web is disabled while Serve is starting");
101
102 await act(async () => {
103 __emitMockRemote("server", { hostId: "box", workspace: "/srv/app", state: "ready", localUrl: "http://127.0.0.1:54321/" });
104 await Promise.resolve();
105 });
106 ok(document.body.textContent?.includes("Ready") === true, "Serve ready state renders");
107 ok(openButton()?.hasAttribute("disabled") === false, "Open Remote Web is enabled once Serve is ready");
108
109 // Clicking the entry opens (or re-points) the host web window with the
110 // resolved workspace.
111 await act(async () => {
112 openButton()?.dispatchEvent(new dom.window.MouseEvent("click", { bubbles: true }));
113 await Promise.resolve();
114 });
115 ok(
116 openCalls.length === 1 && openCalls[0].hostId === "box" && openCalls[0].workspace === "/srv/app",
117 `Open Remote Web calls OpenRemoteWorkspace with the last workspace (got ${JSON.stringify(openCalls)})`,
118 );
119
120 // Fresh host: without a configured or last workspace, the SSH login user's
121 // home directory is a safe, enterable zero-configuration fallback.
122 await act(async () => {
123 useRemoteStore.getState().openExplorer("bare");
124 useRemoteStore.getState().setExplorerTab("server");
125 useRemoteStore.getState().setHosts([
126 host,
127 { id: "bare", label: "bare", host: "bare.test", port: 22, user: "dev", identityFile: "", proxyJump: "", defaultWorkspace: "", serveInstall: "auto", useSSHConfig: false },
128 ]);
129 useRemoteStore.getState().applyStatus({ hostId: "bare", state: "connected" });
130 await Promise.resolve();
131 });
132 const workspaceInput = document.querySelector<HTMLInputElement>('input[placeholder="~"]');
133 ok(workspaceInput?.value === "~", "fresh host defaults its workspace to the SSH login home");
134 ok(openButton()?.hasAttribute("disabled") === false, "Open Remote Web is enabled for a fresh host");
135
136 await act(async () => {
137 openButton()?.dispatchEvent(new dom.window.MouseEvent("click", { bubbles: true }));
138 await Promise.resolve();
139 });
140 ok(
141 openCalls.length === 2 && openCalls[1].hostId === "bare" && openCalls[1].workspace === "~",
142 `fresh host opens the SSH login home (got ${JSON.stringify(openCalls)})`,
143 );
144
145 await act(async () => { root.unmount(); });
146 dom.window.close();
147
148 console.log(`\n${passed} passed, ${failed} failed, ${passed + failed} total`);
149 if (failed > 0) process.exit(1);
150
150 lines Plain Text