| 1 | // Run: tsx src/__tests__/browser-dock-mode.test.tsx |
| 2 | import assert from "node:assert/strict"; |
| 3 | import React, { act } from "react"; |
| 4 | import { createRoot } from "react-dom/client"; |
| 5 | import { JSDOM } from "jsdom"; |
| 6 | |
| 7 | import { WorkspaceDockRegion, type WorkspaceDockRegionProps } from "../app-shell/WorkspaceDockRegion"; |
| 8 | import type { DesktopBrowserHost } from "../lib/browserHost"; |
| 9 | import type { ReasonixDesktopHost } from "../lib/desktopHost"; |
| 10 | import { LocaleProvider, type Translator } from "../lib/i18n"; |
| 11 | import { availableDockEntries } from "../lib/dockEntries"; |
| 12 | import { useLayoutStore, type RightDockMode } from "../store/layout"; |
| 13 | import { useActivityBarStore } from "../store/activityBar"; |
| 14 | |
| 15 | const dom = new JSDOM("<div id='root'></div>", { url: "http://localhost", pretendToBeVisual: true }); |
| 16 | class TestResizeObserver { observe() {} unobserve() {} disconnect() {} } |
| 17 | Object.assign(globalThis, { window: dom.window, document: dom.window.document, localStorage: dom.window.localStorage, |
| 18 | IS_REACT_ACT_ENVIRONMENT: true, ResizeObserver: TestResizeObserver }); |
| 19 | (dom.window as unknown as { ResizeObserver: unknown }).ResizeObserver = TestResizeObserver; |
| 20 | |
| 21 | const noop = () => {}; |
| 22 | const browser: DesktopBrowserHost = { |
| 23 | list: async () => [], open: async () => { throw new Error("unused"); }, close: async () => {}, activate: async () => {}, |
| 24 | navigate: async () => {}, setZoom: async () => {}, toggleDevTools: async () => {}, resume: async () => {}, takeover: async () => {}, |
| 25 | setLayout: noop, setOverlay: noop, onTabs: () => noop, onDownload: () => noop, |
| 26 | }; |
| 27 | const electron: ReasonixDesktopHost = { |
| 28 | kind: "electron", |
| 29 | contract: { protocolVersion: 1, digest: "test", commands: [] }, |
| 30 | platform: { os: "linux", arch: "x64", versions: {} }, |
| 31 | invoke: async () => undefined, |
| 32 | on: () => noop, |
| 33 | native: { |
| 34 | openExternal: async () => {}, |
| 35 | clipboard: { writeText: async () => true, readText: async () => "" }, |
| 36 | window: { setTheme: noop, setBackgroundColour: noop, getBounds: async () => ({ x: 0, y: 0, width: 0, height: 0, maximised: false }), |
| 37 | isMaximised: async () => false, minimise: noop, toggleMaximise: noop, close: noop }, |
| 38 | getPathForFile: () => "", |
| 39 | onServiceState: () => noop, |
| 40 | }, |
| 41 | browser, |
| 42 | }; |
| 43 | |
| 44 | const modes: RightDockMode[] = []; |
| 45 | const picks: string[] = []; |
| 46 | const props = (mode: RightDockMode): WorkspaceDockRegionProps => ({ |
| 47 | visible: true, overlay: false, mode, showContext: true, |
| 48 | t: ((key: string) => key) as Translator, |
| 49 | onPickEntry: (entryId: string) => { picks.push(entryId); }, |
| 50 | remote: {} as WorkspaceDockRegionProps["remote"], context: {} as WorkspaceDockRegionProps["context"], |
| 51 | workspace: { tabId: "A" } as WorkspaceDockRegionProps["workspace"], workspaceKey: "k", |
| 52 | }); |
| 53 | const root = createRoot(document.getElementById("root")!); |
| 54 | const paint = (mode: RightDockMode) => act(async () => root.render(<LocaleProvider><WorkspaceDockRegion {...props(mode)} /></LocaleProvider>)); |
| 55 | const tabLabels = () => [...document.querySelectorAll(".workbench-dock__tab-label")].map((el) => el.textContent); |
| 56 | const settle = () => act(async () => { await new Promise((resolve) => setTimeout(resolve, 20)); }); |
| 57 | |
| 58 | console.log("\nbrowser dock mode"); |
| 59 | try { |
| 60 | await paint("files"); |
| 61 | assert.deepEqual(tabLabels(), [], "an empty tab list renders no dock tabs"); |
| 62 | assert.equal(availableDockEntries(false).some((entry) => entry.defaultTab === "browser"), false, |
| 63 | "no shell host: the browser entry is not offered"); |
| 64 | assert.deepEqual(modes, [], "an empty dock issues no mode command"); |
| 65 | |
| 66 | // With no tab open the dock offers the tab picker instead of a blank panel. |
| 67 | const picker = document.querySelector(".tab-picker"); |
| 68 | assert.ok(picker, "an empty dock renders the tab picker"); |
| 69 | const pickerEntries = [...picker!.querySelectorAll<HTMLButtonElement>(".tab-picker__item")]; |
| 70 | assert.deepEqual(pickerEntries.map((button) => button.textContent), ["Overview", "Files", "Changes"], |
| 71 | "the picker lists every openable view, browser excluded without a shell host"); |
| 72 | await act(async () => pickerEntries[0].click()); |
| 73 | assert.deepEqual(picks, ["context"], "picking an entry opens that view through the shared command"); |
| 74 | |
| 75 | window.reasonixDesktop = electron; |
| 76 | assert.equal(availableDockEntries(true).some((entry) => entry.defaultTab === "browser"), true, |
| 77 | "the Electron host offers the browser entry"); |
| 78 | |
| 79 | await act(async () => { useActivityBarStore.getState().addTab("browser", "Browser"); }); |
| 80 | await paint("browser"); |
| 81 | await settle(); |
| 82 | assert.deepEqual(tabLabels(), ["Browser"], "the browser tab renders in the dock"); |
| 83 | assert.ok(document.querySelector(".browser-panel"), "the browser tab mounts the lazy panel in the dock body"); |
| 84 | assert.ok(document.querySelector(".workbench-dock--browser"), "the dock carries the mode modifier"); |
| 85 | |
| 86 | useLayoutStore.getState().setRightDockMode("browser"); |
| 87 | assert.equal(useLayoutStore.getState().rightDockMode, "browser", "the layout store accepts the browser dock mode"); |
| 88 | |
| 89 | const browserTabId = useActivityBarStore.getState().tabs[0].id; |
| 90 | await act(async () => { useActivityBarStore.getState().closeTab(browserTabId); }); |
| 91 | await paint("files"); |
| 92 | await settle(); |
| 93 | assert.equal(document.querySelector(".browser-panel"), null, "closing the browser tab unmounts the panel"); |
| 94 | |
| 95 | delete window.reasonixDesktop; |
| 96 | await act(async () => root.unmount()); |
| 97 | console.log("browser dock mode: gating, tab render and lazy mount passed"); |
| 98 | } finally { |
| 99 | dom.window.close(); |
| 100 | } |
| 101 |