返回 DeepSeek-Reasonix
remote-error-ux.test.tsx
根目录 / desktop / frontend / src / __tests__ / remote-error-ux.test.tsx
1 // Run: tsx src/__tests__/remote-error-ux.test.tsx
2
3 import React from "react";
4 import { JSDOM } from "jsdom";
5 import { act } from "react";
6 import { createRoot } from "react-dom/client";
7
8 import { StatusBar } from "../components/StatusBar";
9 import { LocaleProvider } from "../lib/i18n";
10 import type { RemoteConnectionStatus, RemoteHostView } from "../lib/types";
11 import { useRemoteStore } from "../store/remote";
12
13 let passed = 0;
14 let failed = 0;
15
16 function ok(value: boolean, label: string) {
17 if (value) {
18 process.stdout.write(` PASS ${label}\n`);
19 passed += 1;
20 } else {
21 process.stdout.write(` FAIL ${label}\n`);
22 failed += 1;
23 }
24 }
25
26 const dom = new JSDOM("<!doctype html><html><body><div id=\"root\"></div></body></html>", {
27 pretendToBeVisual: true,
28 });
29 (globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
30 globalThis.window = dom.window as unknown as Window & typeof globalThis;
31 globalThis.document = dom.window.document;
32 // Pin the locale source to the JSDOM navigator (en-US): Node's own global
33 // navigator follows the machine's system language, which flips detectLocale
34 // to Chinese on zh hosts and breaks the English assertions below.
35 Object.defineProperty(globalThis, "navigator", { configurable: true, value: dom.window.navigator });
36 globalThis.Node = dom.window.Node;
37 globalThis.HTMLElement = dom.window.HTMLElement;
38 globalThis.Event = dom.window.Event;
39 globalThis.KeyboardEvent = dom.window.KeyboardEvent;
40 globalThis.MouseEvent = dom.window.MouseEvent;
41 globalThis.requestAnimationFrame = dom.window.requestAnimationFrame.bind(dom.window);
42 globalThis.cancelAnimationFrame = dom.window.cancelAnimationFrame.bind(dom.window);
43 Object.defineProperty(window, "matchMedia", {
44 configurable: true,
45 value: () => ({ matches: true, addEventListener() {}, removeEventListener() {} }),
46 });
47
48 const host: RemoteHostView = {
49 id: "box",
50 label: "Build box",
51 host: "example.test",
52 port: 2222,
53 user: "dev",
54 identityFile: "",
55 proxyJump: "",
56 defaultWorkspace: "/srv/app",
57 serveInstall: "auto",
58 credentialMode: "remote",
59 useSSHConfig: false,
60 };
61 const rawError = "remote: host key mismatch (/home/dev/.ssh/known_hosts:7)";
62 const status: RemoteConnectionStatus = {
63 hostId: "box",
64 state: "stopped",
65 error: rawError,
66 errorDetails: {
67 code: "host_key_mismatch",
68 presentedSha256: "SHA256:new",
69 knownHostRecords: [{ path: "/home/dev/.ssh/known_hosts", line: 7 }],
70 },
71 };
72 const degradedStatus: RemoteConnectionStatus = {
73 hostId: "box",
74 state: "degraded",
75 error: "forward attach failed",
76 };
77
78 useRemoteStore.setState({ statusPopoverRequest: null });
79 const rootEl = document.getElementById("root");
80 if (!rootEl) throw new Error("missing root");
81 const root = createRoot(rootEl);
82
83 await act(async () => {
84 root.render(
85 <LocaleProvider>
86 <StatusBar
87 context={{ used: 0, window: 0, sessionTokens: 0 }}
88 running={false}
89 remoteHosts={[host]}
90 remoteStatuses={{ box: status }}
91 />
92 </LocaleProvider>,
93 );
94 });
95
96 await act(async () => {
97 useRemoteStore.getState().requestStatusPopover("box");
98 await new Promise((resolve) => requestAnimationFrame(() => resolve(undefined)));
99 });
100
101 const card = document.querySelector<HTMLElement>(".remote-switcher__error-card");
102 ok(Boolean(card), "terminal connection failure opens the anchored Remote SSH popover");
103 ok(card?.textContent?.includes("host key differs from the previous record") === true, "popover uses a localized security summary");
104 ok(card?.textContent?.includes("/home/dev/.ssh/known_hosts") === false, "primary error card hides machine-local diagnostics");
105
106 const detailsButton = Array.from(card?.querySelectorAll("button") ?? []).find((button) => button.textContent?.includes("View key details"));
107 await act(async () => {
108 detailsButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
109 });
110
111 const dialog = document.querySelector<HTMLElement>(".remote-connection-error-dialog");
112 ok(Boolean(dialog), "key-details action opens the security dialog");
113 ok(dialog?.textContent?.includes("SHA256:new") === true, "security dialog shows the presented fingerprint");
114 ok(dialog?.textContent?.includes("/home/dev/.ssh/known_hosts:7") === true, "security dialog shows the conflicting known_hosts record");
115
116 const closeButton = Array.from(dialog?.querySelectorAll("button") ?? []).find((button) => button.textContent?.includes("Close"));
117 await act(async () => {
118 closeButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
119 root.render(
120 <LocaleProvider>
121 <StatusBar
122 context={{ used: 0, window: 0, sessionTokens: 0 }}
123 running={false}
124 remoteHosts={[host]}
125 remoteStatuses={{ box: degradedStatus }}
126 />
127 </LocaleProvider>,
128 );
129 useRemoteStore.getState().requestStatusPopover("box");
130 await new Promise((resolve) => requestAnimationFrame(() => resolve(undefined)));
131 });
132
133 const warningCard = document.querySelector<HTMLElement>(".remote-switcher__error-card--warning");
134 ok(Boolean(warningCard), "degraded connection uses a warning card");
135 ok(warningCard?.textContent?.includes("SSH is connected") === true, "degraded warning explains that SSH remains connected");
136 ok(warningCard?.textContent?.includes("Connection failed") === false, "degraded warning does not claim the connection failed");
137
138 await act(async () => root.unmount());
139 dom.window.close();
140
141 process.stdout.write(`\n${passed} passed, ${failed} failed\n`);
142 if (failed > 0) process.exit(1);
143
143 lines Plain Text