返回 DeepSeek-Reasonix
remote-tab-opened.test.tsx
根目录 / desktop / frontend / src / __tests__ / remote-tab-opened.test.tsx
1 // Run: tsx src/__tests__/remote-tab-opened.test.tsx
2
3 import { JSDOM } from "jsdom";
4 import React, { act, useRef, useState } from "react";
5 import { createRoot } from "react-dom/client";
6 import { __emitMockRemoteTabOpened, __emitMockRemoteTabUpdated, app } from "../lib/bridge";
7 import type { TabMeta } from "../lib/types";
8 import type { RemoteSessionApi } from "../lib/useRemoteSession";
9 import { useRemoteSession } from "../lib/useRemoteSession";
10 import { useRemoteTabOpened } from "../lib/useRemoteTabOpened";
11 import { useRemoteTabSwitch } from "../lib/useRemoteTabSwitch";
12 import { installDesktopHostStub } from "./desktopHostStub";
13
14 let passed = 0;
15 let failed = 0;
16
17 function eq(actual: unknown, expected: unknown, label: string) {
18 if (actual === expected) {
19 process.stdout.write(` PASS ${label}\n`);
20 passed += 1;
21 } else {
22 process.stdout.write(` FAIL ${label}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}\n`);
23 failed += 1;
24 }
25 }
26
27 console.log("\nRemote tab opened/updated routing");
28
29 const dom = new JSDOM("<!doctype html><html><body><div id=\"root\"></div></body></html>", {
30 pretendToBeVisual: true,
31 url: "http://localhost/",
32 });
33 (globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
34 globalThis.window = dom.window as unknown as Window & typeof globalThis;
35 globalThis.document = dom.window.document;
36
37 const seeded: string[] = [];
38 const updated: string[] = [];
39 const switched: string[] = [];
40 const remoteMeta: TabMeta = {
41 id: "remote-1",
42 scope: "project",
43 workspaceRoot: "remote-project:host-a:/repo",
44 workspaceName: "repo",
45 workspacePath: "/repo",
46 gitBranch: "main",
47 label: "model",
48 ready: true,
49 running: false,
50 active: false,
51 remote: { hostId: "host-a", workspace: "/repo" },
52 };
53
54 function Harness() {
55 useRemoteTabOpened(
56 (meta) => seeded.push(meta.id),
57 (meta) => updated.push(meta.id),
58 );
59 return null;
60 }
61
62 const root = createRoot(document.getElementById("root")!);
63 await act(async () => root.render(<Harness />));
64 await act(async () => __emitMockRemoteTabOpened(remoteMeta));
65 eq(seeded.join(","), "remote-1", "opened events seed the new remote tab metadata");
66 eq(switched.join(","), "", "opened notifications cannot acquire navigation ownership");
67
68 await act(async () => __emitMockRemoteTabUpdated({ ...remoteMeta, topicTitle: "Background title" }));
69 eq(updated.join(","), "remote-1", "metadata updates patch the remote tab");
70 eq(switched.join(","), "", "metadata updates never steal focus");
71
72 await act(async () => root.unmount());
73
74 let directSwitch: ((meta: TabMeta) => Promise<void>) | undefined;
75 let historyCalls = 0;
76 let activeCalls = 0;
77 let releaseNavigationRegistration: (() => void) | undefined;
78 let navigationRegistration = new Promise<void>((resolve) => { releaseNavigationRegistration = resolve; });
79 const originalHistory = app.HistorySliceForTab;
80 const desktopStub = installDesktopHostStub(({ main: { App: {
81 HistorySliceForTab: async (...args: Parameters<typeof originalHistory>) => {
82 historyCalls += 1;
83 return originalHistory(...args);
84 },
85 SetActiveTab: async () => { activeCalls += 1; },
86 } as unknown as typeof app } }).main.App);
87
88 function SwitchHarness() {
89 const [activeId, setActiveId] = useState<string | undefined>("local-1");
90 const activeIdRef = useRef<string | undefined>(activeId);
91 activeIdRef.current = activeId;
92 directSwitch = useRemoteTabSwitch({
93 activeTabIdRef: activeIdRef,
94 setActiveTabId: setActiveId,
95 beginNavigation: () => 1,
96 requireRegisteredNavigation: () => navigationRegistration,
97 navigationCanComplete: () => true,
98 navigationIsCurrent: () => true,
99 confirmBackendActiveTab: () => undefined,
100 reassertVisibleTab: async () => undefined,
101 });
102 return <span data-active-id={activeId} />;
103 }
104
105 const switchRoot = createRoot(document.getElementById("root")!);
106 await act(async () => switchRoot.render(<SwitchHarness />));
107 let directSwitchPromise: Promise<void> | undefined;
108 await act(async () => {
109 directSwitchPromise = directSwitch?.(remoteMeta);
110 await Promise.resolve();
111 });
112 eq(activeCalls, 0, "remote activation waits for navigation registration before backend focus");
113 releaseNavigationRegistration?.();
114 await act(async () => directSwitchPromise);
115 eq(document.querySelector("span")?.getAttribute("data-active-id"), "remote-1", "remote activation updates the selected tab");
116 eq(activeCalls, 1, "remote activation still binds backend focus");
117 eq(historyCalls, 0, "remote activation bypasses local history hydration");
118 await act(async () => switchRoot.unmount());
119 navigationRegistration = Promise.resolve();
120
121 let terminalProbe: RemoteSessionApi | undefined;
122 desktopStub.replaceCommands(({ main: { App: {
123 RemoteTabSnapshot: async () => { throw new Error("serve unavailable"); },
124 SetActiveTab: async (tabId: string) => {
125 desktopStub.emit(`remote-tab:${tabId}:state`, { state: "serve_down", error: "bootstrap failed" });
126 },
127 } as unknown as typeof app } }).main.App);
128 function TerminalHarness() { terminalProbe = useRemoteSession("remote-terminal", "disconnected"); return null; }
129 const terminalRoot = createRoot(document.getElementById("root")!);
130 await act(async () => terminalRoot.render(<TerminalHarness />));
131 eq(terminalProbe?.state, "serve_down", "restored shell observes terminal state republished during activation");
132 await act(async () => terminalRoot.unmount());
133 desktopStub.uninstall();
134 process.stdout.write(`\n${passed} passed, ${failed} failed\n`);
135 if (failed > 0) process.exit(1);
136
136 lines Plain Text