返回 DeepSeek-Reasonix
remote-secret-dialog.test.tsx
根目录 / desktop / frontend / src / __tests__ / remote-secret-dialog.test.tsx
1 // Run: tsx src/__tests__/remote-secret-dialog.test.tsx
2
3 import React from "react";
4 import { JSDOM } from "jsdom";
5 import { act } from "react";
6
7 import type { AppBindings } from "../lib/bridge";
8 import { installDesktopHostStub } from "./desktopHostStub";
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 one-shot credential prompt");
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 // React's legacy input-event fallback expects these IE hooks when JSDOM does
35 // not advertise native InputEvent support.
36 Object.defineProperty(dom.window.HTMLElement.prototype, "attachEvent", { configurable: true, value: () => {} });
37 Object.defineProperty(dom.window.HTMLElement.prototype, "detachEvent", { configurable: true, value: () => {} });
38
39 // Import ReactDOM and the component only after installing the DOM so React
40 // selects its native input-event path instead of the legacy IE polyfill.
41 const [{ createRoot }, { RemoteSecretDialog }, { LocaleProvider }, { useRemoteStore }] = await Promise.all([
42 import("react-dom/client"),
43 import("../components/RemoteSecretDialog"),
44 import("../lib/i18n"),
45 import("../store/remote"),
46 ]);
47
48 const calls: Array<{ hostId: string; promptId: string; secret: string; accept: boolean }> = [];
49 installDesktopHostStub(({ main: { App: {
50 async ConfirmRemoteSecret(hostId: string, promptId: string, secret: string, accept: boolean) {
51 calls.push({ hostId, promptId, secret, accept });
52 },
53 } as Partial<AppBindings> as AppBindings } }).main.App);
54
55 const rootElement = document.getElementById("root");
56 if (!rootElement) throw new Error("missing root");
57 const root = createRoot(rootElement);
58 await act(async () => root.render(<LocaleProvider><RemoteSecretDialog /></LocaleProvider>));
59
60 await act(async () => {
61 useRemoteStore.getState().applyStatus({
62 hostId: "box",
63 state: "pending_secret",
64 secretPrompt: { promptId: "prompt-1", hostId: "box", host: "dev@box.test", kind: "password" },
65 });
66 });
67 const input = document.querySelector<HTMLInputElement>('input[type="password"]');
68 ok(Boolean(input), "password request opens a masked input dialog");
69 ok(document.body.textContent?.includes("dev@box.test") === true, "dialog identifies the SSH target");
70
71 await act(async () => {
72 if (!input) return;
73 const setter = Object.getOwnPropertyDescriptor(dom.window.HTMLInputElement.prototype, "value")?.set;
74 setter?.call(input, "one-shot-secret");
75 input.dispatchEvent(new dom.window.Event("input", { bubbles: true }));
76 input.dispatchEvent(new dom.window.Event("change", { bubbles: true }));
77 await Promise.resolve();
78 });
79 await act(async () => {
80 if (!input) return;
81 input.closest("form")?.dispatchEvent(new dom.window.Event("submit", { bubbles: true, cancelable: true }));
82 await Promise.resolve();
83 });
84 ok(calls.length === 1 && calls[0]?.promptId === "prompt-1" && calls[0]?.secret === "one-shot-secret" && calls[0]?.accept === true, "submit sends the prompt ID and secret once to the native bridge");
85 ok(useRemoteStore.getState().pendingSecretPrompt === null, "resolved prompt is removed from shared UI state");
86 ok(document.body.textContent?.includes("one-shot-secret") === false, "secret plaintext is never rendered into the page");
87
88 await act(async () => root.unmount());
89 dom.window.close();
90 process.stdout.write(`\n${passed} passed, ${failed} failed\n`);
91 if (failed > 0) process.exit(1);
92
92 lines Plain Text