返回 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 { JSDOM } from "jsdom";
4 import React from "react";
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 globalThis.Node = dom.window.Node;
33 globalThis.HTMLElement = dom.window.HTMLElement;
34 globalThis.Event = dom.window.Event;
35 globalThis.KeyboardEvent = dom.window.KeyboardEvent;
36 globalThis.MouseEvent = dom.window.MouseEvent;
37 globalThis.requestAnimationFrame = dom.window.requestAnimationFrame.bind(dom.window);
38 globalThis.cancelAnimationFrame = dom.window.cancelAnimationFrame.bind(dom.window);
39 Object.defineProperty(window, "matchMedia", {
40 configurable: true,
41 value: () => ({ matches: true, addEventListener() {}, removeEventListener() {} }),
42 });
43
44 const host: RemoteHostView = {
45 id: "box",
46 label: "Build box",
47 host: "example.test",
48 port: 2222,
49 user: "dev",
50 identityFile: "",
51 proxyJump: "",
52 defaultWorkspace: "/srv/app",
53 serveInstall: "auto",
54 useSSHConfig: false,
55 };
56 const rawError = "remote: host key mismatch (/home/dev/.ssh/known_hosts:7)";
57 const status: RemoteConnectionStatus = {
58 hostId: "box",
59 state: "stopped",
60 error: rawError,
61 errorDetails: {
62 code: "host_key_mismatch",
63 presentedSha256: "SHA256:new",
64 knownHostRecords: [{ path: "/home/dev/.ssh/known_hosts", line: 7 }],
65 },
66 };
67 const degradedStatus: RemoteConnectionStatus = {
68 hostId: "box",
69 state: "degraded",
70 error: "forward attach failed",
71 };
72
73 useRemoteStore.setState({ statusPopoverRequest: null });
74 const rootEl = document.getElementById("root");
75 if (!rootEl) throw new Error("missing root");
76 const root = createRoot(rootEl);
77
78 await act(async () => {
79 root.render(
80 <LocaleProvider>
81 <StatusBar
82 context={{ used: 0, window: 0, sessionTokens: 0 }}
83 running={false}
84 remoteHosts={[host]}
85 remoteStatuses={{ box: status }}
86 />
87 </LocaleProvider>,
88 );
89 });
90
91 await act(async () => {
92 useRemoteStore.getState().requestStatusPopover("box");
93 await new Promise((resolve) => requestAnimationFrame(() => resolve(undefined)));
94 });
95
96 const card = document.querySelector<HTMLElement>(".remote-switcher__error-card");
97 ok(Boolean(card), "terminal connection failure opens the anchored Remote SSH popover");
98 ok(card?.textContent?.includes("host key differs from the previous record") === true, "popover uses a localized security summary");
99 ok(card?.textContent?.includes("/home/dev/.ssh/known_hosts") === false, "primary error card hides machine-local diagnostics");
100
101 const detailsButton = Array.from(card?.querySelectorAll("button") ?? []).find((button) => button.textContent?.includes("View key details"));
102 await act(async () => {
103 detailsButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
104 });
105
106 const dialog = document.querySelector<HTMLElement>(".remote-connection-error-dialog");
107 ok(Boolean(dialog), "key-details action opens the security dialog");
108 ok(dialog?.textContent?.includes("SHA256:new") === true, "security dialog shows the presented fingerprint");
109 ok(dialog?.textContent?.includes("/home/dev/.ssh/known_hosts:7") === true, "security dialog shows the conflicting known_hosts record");
110
111 const closeButton = Array.from(dialog?.querySelectorAll("button") ?? []).find((button) => button.textContent?.includes("Close"));
112 await act(async () => {
113 closeButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
114 root.render(
115 <LocaleProvider>
116 <StatusBar
117 context={{ used: 0, window: 0, sessionTokens: 0 }}
118 running={false}
119 remoteHosts={[host]}
120 remoteStatuses={{ box: degradedStatus }}
121 />
122 </LocaleProvider>,
123 );
124 useRemoteStore.getState().requestStatusPopover("box");
125 await new Promise((resolve) => requestAnimationFrame(() => resolve(undefined)));
126 });
127
128 const warningCard = document.querySelector<HTMLElement>(".remote-switcher__error-card--warning");
129 ok(Boolean(warningCard), "degraded connection uses a warning card");
130 ok(warningCard?.textContent?.includes("SSH is connected") === true, "degraded warning explains that SSH remains connected");
131 ok(warningCard?.textContent?.includes("Connection failed") === false, "degraded warning does not claim the connection failed");
132
133 await act(async () => root.unmount());
134 dom.window.close();
135
136 process.stdout.write(`\n${passed} passed, ${failed} failed\n`);
137 if (failed > 0) process.exit(1);
138
138 lines Plain Text