| 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 |