import { beforeEach, describe, expect, it, vi } from "vitest"; // Mock renderer module - must be hoisted before importing main const rendererMocks = vi.hoisted(() => { const getRendererUrl = vi.fn(() => "file:///path/to/dist/client/index.html"); return { isDevelopmentMode: vi.fn(() => false), getRendererUrl, getRendererFilePath: vi.fn(() => "/path/to/dist/client/index.html"), isUrlRenderer: vi.fn(() => false), IS_DEVELOPMENT: false, // DASHBOARD_URL is re-exported as getRendererUrl DASHBOARD_URL: getRendererUrl, }; }); vi.mock("../renderer.ts", () => rendererMocks); const mocks = vi.hoisted(() => { const browserWindowHandlers = new Map void>(); const browserWindowInstance = { loadURL: vi.fn(), loadFile: vi.fn(), on: vi.fn((event: string, handler: (...args: unknown[]) => void) => { browserWindowHandlers.set(event, handler); }), once: vi.fn((event: string, handler: (...args: unknown[]) => void) => { browserWindowHandlers.set(event, handler); }), isVisible: vi.fn(() => true), show: vi.fn(), focus: vi.fn(), hide: vi.fn(), maximize: vi.fn(), }; const BrowserWindow = vi.fn(() => browserWindowInstance) as unknown as { (...args: unknown[]): typeof browserWindowInstance; getAllWindows: () => unknown[]; }; BrowserWindow.getAllWindows = vi.fn(() => []); const app = { whenReady: vi.fn(() => Promise.resolve()), getVersion: vi.fn(() => "0.1.0"), quit: vi.fn(), on: vi.fn(), }; const ipcMain = { handle: vi.fn(), on: vi.fn(), }; const trayInstance = { setImage: vi.fn(), setToolTip: vi.fn(), setContextMenu: vi.fn(), on: vi.fn(), }; const Tray = vi.fn(() => trayInstance); const Menu = { buildFromTemplate: vi.fn(() => ({ id: "mock-menu" })), setApplicationMenu: vi.fn(), }; const nativeImage = { createEmpty: vi.fn(() => ({ id: "mock-image" })), createFromPath: vi.fn(() => ({ resize: vi.fn(() => ({ id: "resized-image" })), })), }; const shell = { openExternal: vi.fn(() => Promise.resolve()), }; const isDevelopmentMode = vi.fn(() => false); const getRendererUrl = vi.fn(() => "file:///path/to/dist/client/index.html"); const getRendererFilePath = vi.fn(() => "/path/to/dist/client/index.html"); const isUrlRenderer = vi.fn(() => false); const screen = { getAllDisplays: vi.fn(() => [{ workArea: { x: 0, y: 0, width: 1920, height: 1080 } }]), }; return { app, BrowserWindow, ipcMain, trayInstance, Tray, Menu, nativeImage, shell, screen, browserWindowInstance, browserWindowHandlers, isDevelopmentMode, getRendererUrl, getRendererFilePath, isUrlRenderer, }; }); vi.mock("electron", () => ({ app: mocks.app, BrowserWindow: mocks.BrowserWindow, ipcMain: mocks.ipcMain, Tray: mocks.Tray, Menu: mocks.Menu, nativeImage: mocks.nativeImage, shell: mocks.shell, screen: mocks.screen, })); const mainDeps = vi.hoisted(() => { const startLocal = vi.fn(async () => ({ source: "embedded-local", state: "running", port: 4545 })); const stopLocal = vi.fn(async () => ({ source: "none", state: "stopped" })); const getStatus = vi.fn(() => ({ source: "none", state: "stopped" })); const getServerPort = vi.fn(() => 0); const loadDesktopLaunchMode = vi.fn(async () => "choose"); const saveDesktopLaunchMode = vi.fn(async () => undefined); return { registerIpcHandlers: vi.fn(), buildAppMenu: vi.fn(), setupTray: vi.fn(), registerDeepLinkProtocol: vi.fn(), setupDeepLinkHandler: vi.fn(), setupAutoUpdater: vi.fn(), loadWindowState: vi.fn(async () => null), loadDesktopLaunchMode, saveDesktopLaunchMode, saveWindowState: vi.fn(), LocalRuntimeManager: vi.fn(() => ({ startLocal, stopLocal, getStatus, getServerPort })), startLocal, }; }); vi.mock("../ipc.js", () => ({ registerIpcHandlers: mainDeps.registerIpcHandlers })); vi.mock("../menu.js", () => ({ buildAppMenu: mainDeps.buildAppMenu })); vi.mock("../tray.js", () => ({ setupTray: mainDeps.setupTray })); vi.mock("../deep-link.js", () => ({ registerDeepLinkProtocol: mainDeps.registerDeepLinkProtocol, setupDeepLinkHandler: mainDeps.setupDeepLinkHandler, })); vi.mock("../native.js", () => ({ DEFAULT_WINDOW_STATE: { width: 1280, height: 900, isMaximized: false }, loadWindowState: mainDeps.loadWindowState, loadDesktopLaunchMode: mainDeps.loadDesktopLaunchMode, saveDesktopLaunchMode: mainDeps.saveDesktopLaunchMode, saveWindowState: mainDeps.saveWindowState, setupAutoUpdater: mainDeps.setupAutoUpdater, startUpdateCheckInterval: vi.fn(() => vi.fn()), clampWindowStateToVisibleDisplay: vi.fn((state, displays) => { if (state.x === undefined || state.y === undefined) { return state; } const minVisible = 64; const windowRight = state.x + state.width; const windowBottom = state.y + state.height; const visible = displays.some((display: { workArea: { x: number; y: number; width: number; height: number } }) => { const right = display.workArea.x + display.workArea.width; const bottom = display.workArea.y + display.workArea.height; const overlapWidth = Math.max(0, Math.min(windowRight, right) - Math.max(state.x, display.workArea.x)); const overlapHeight = Math.max(0, Math.min(windowBottom, bottom) - Math.max(state.y, display.workArea.y)); return overlapWidth >= minVisible && overlapHeight >= minVisible; }); if (visible) { return state; } return { width: state.width, height: state.height, isMaximized: state.isMaximized }; }), normalizeDesktopRemoteLaunch: vi.fn((settings) => { const active = settings.profiles.find((profile: { id: string }) => profile.id === settings.activeProfileId); return active ? { mode: "remote", profileId: active.id, serverBaseUrl: active.serverUrl.replace(/\/$/, ""), serverLabel: active.name, authToken: active.authToken ?? undefined } : null; }), buildRemoteShellHandoffUrl: vi.fn((launch) => `https://remote.example.com?shellKind=desktop&shellMode=remote&profileId=${launch.profileId}`), })); vi.mock("../local-runtime.js", () => ({ LocalRuntimeManager: mainDeps.LocalRuntimeManager, })); vi.mock("../shell-settings.js", () => ({ readShellSettings: vi.fn(async () => ({ desktopMode: "remote", activeProfileId: "profile_1", profiles: [{ id: "profile_1", name: "Remote", serverUrl: "https://remote.example.com", authToken: "token" }], })), })); async function importMainModule() { return import("../main.ts"); } describe("main process", () => { const originalDashboardUrl = process.env.FUSION_DASHBOARD_URL; const originalNodeEnv = process.env.NODE_ENV; beforeEach(() => { vi.clearAllMocks(); vi.resetModules(); delete process.env.FUSION_DESKTOP_MODE; if (originalDashboardUrl === undefined) { delete process.env.FUSION_DASHBOARD_URL; } else { process.env.FUSION_DASHBOARD_URL = originalDashboardUrl; } if (originalNodeEnv === undefined) { delete process.env.NODE_ENV; } else { process.env.NODE_ENV = originalNodeEnv; } // Ensure we're in production mode for these tests rendererMocks.isDevelopmentMode.mockReturnValue(false); rendererMocks.getRendererUrl.mockReturnValue("file:///path/to/dist/client/index.html"); rendererMocks.getRendererFilePath.mockReturnValue("/path/to/dist/client/index.html"); rendererMocks.isUrlRenderer.mockReturnValue(false); mocks.screen.getAllDisplays.mockReturnValue([{ workArea: { x: 0, y: 0, width: 1920, height: 1080 } }]); }); it("DASHBOARD_URL defaults to local file URL in production mode", async () => { delete process.env.FUSION_DASHBOARD_URL; const { DASHBOARD_URL } = await importMainModule(); expect(DASHBOARD_URL()).toMatch(/^file:\/\/.*\/client\/index\.html$/); }); it("DASHBOARD_URL uses env override in development mode", async () => { process.env.FUSION_DASHBOARD_URL = "http://localhost:5050"; // Mock development mode to use the env var rendererMocks.isDevelopmentMode.mockReturnValue(true); rendererMocks.getRendererUrl.mockReturnValue("http://localhost:5050"); rendererMocks.getRendererFilePath.mockReturnValue(""); rendererMocks.isUrlRenderer.mockReturnValue(true); const { DASHBOARD_URL } = await importMainModule(); expect(DASHBOARD_URL()).toBe("http://localhost:5050"); }); it("createMainWindow creates BrowserWindow with secure preferences", async () => { const { createMainWindow } = await importMainModule(); createMainWindow(); expect(mocks.BrowserWindow).toHaveBeenCalledTimes(1); const [options] = mocks.BrowserWindow.mock.calls[0] as [ { webPreferences: { contextIsolation: boolean; nodeIntegration: boolean; preload: string; }; }, ]; expect(options.webPreferences.contextIsolation).toBe(true); expect(options.webPreferences.nodeIntegration).toBe(false); expect(options.webPreferences.preload).toContain("preload.js"); }); it("createMainWindow loads the renderer URL in URL mode", async () => { rendererMocks.isUrlRenderer.mockReturnValue(true); rendererMocks.getRendererUrl.mockReturnValue("http://localhost:3000/index.html"); rendererMocks.getRendererFilePath.mockReturnValue(""); const { createMainWindow } = await importMainModule(); createMainWindow(); expect(mocks.browserWindowInstance.loadURL).toHaveBeenCalledWith("http://localhost:3000/index.html"); expect(mocks.browserWindowInstance.loadFile).not.toHaveBeenCalled(); }); it("createMainWindow loads the renderer file in file mode (production)", async () => { rendererMocks.isUrlRenderer.mockReturnValue(false); rendererMocks.getRendererUrl.mockReturnValue("file:///path/to/dist/client/index.html"); rendererMocks.getRendererFilePath.mockReturnValue("/path/to/dist/client/index.html"); const { createMainWindow } = await importMainModule(); createMainWindow(); expect(mocks.browserWindowInstance.loadFile).toHaveBeenCalledWith("/path/to/dist/client/index.html"); expect(mocks.browserWindowInstance.loadURL).not.toHaveBeenCalled(); }); it("exports initializeApp for lifecycle orchestration", async () => { const mainModule = await importMainModule(); expect(typeof mainModule.initializeApp).toBe("function"); }); it("initializeApp starts local runtime when remembered mode is local", async () => { mainDeps.loadDesktopLaunchMode.mockResolvedValueOnce("local"); const { initializeApp, getCurrentDesktopLaunchMode } = await importMainModule(); await initializeApp(); expect(mainDeps.startLocal).toHaveBeenCalledTimes(1); expect(getCurrentDesktopLaunchMode()).toBe("local"); }); it("initializeApp does not start local runtime for remembered choose mode", async () => { mainDeps.loadDesktopLaunchMode.mockResolvedValueOnce("choose"); const { initializeApp } = await importMainModule(); await initializeApp(); expect(mainDeps.startLocal).not.toHaveBeenCalled(); }); it("initializeApp routes remembered remote mode to remote dashboard handoff URL", async () => { mainDeps.loadDesktopLaunchMode.mockResolvedValueOnce("remote"); const { initializeApp, getCurrentDesktopLaunchMode } = await importMainModule(); await initializeApp(); expect(mainDeps.startLocal).not.toHaveBeenCalled(); expect(mocks.browserWindowInstance.loadURL).toHaveBeenCalledWith( expect.stringContaining("shellMode=remote"), ); expect(getCurrentDesktopLaunchMode()).toBe("remote"); }); it("initializeApp falls back to choose and persists when remembered local start fails", async () => { mainDeps.loadDesktopLaunchMode.mockResolvedValueOnce("local"); mainDeps.startLocal.mockRejectedValueOnce(new Error("boom")); const { initializeApp, getCurrentDesktopLaunchMode } = await importMainModule(); await initializeApp(); expect(mainDeps.saveDesktopLaunchMode).toHaveBeenCalledWith("choose"); expect(getCurrentDesktopLaunchMode()).toBe("choose"); }); it("initializeApp avoids duplicate local start when remembered mode and env flag are local", async () => { mainDeps.loadDesktopLaunchMode.mockResolvedValueOnce("local"); process.env.FUSION_DESKTOP_MODE = "local"; const { initializeApp } = await importMainModule(); await initializeApp(); expect(mainDeps.startLocal).toHaveBeenCalledTimes(1); }); it("onDesktopModeChange persists the selected launch mode", async () => { const { initializeApp } = await importMainModule(); await initializeApp(); const options = mainDeps.registerIpcHandlers.mock.calls[0]?.[2] as | { onDesktopModeChange?: (mode: "local" | "remote") => Promise } | undefined; await options?.onDesktopModeChange?.("remote"); expect(mainDeps.saveDesktopLaunchMode).toHaveBeenCalledWith("remote"); }); it("createMainWindow registers close and closed handlers", async () => { const { createMainWindow } = await importMainModule(); createMainWindow(); expect(mocks.browserWindowInstance.on).toHaveBeenCalledWith("close", expect.any(Function)); expect(mocks.browserWindowInstance.on).toHaveBeenCalledWith("closed", expect.any(Function)); }); it("createMainWindow shows and focuses on ready-to-show", async () => { const { createMainWindow } = await importMainModule(); createMainWindow(); mocks.browserWindowHandlers.get("ready-to-show")?.(); expect(mocks.browserWindowInstance.show).toHaveBeenCalledTimes(1); expect(mocks.browserWindowInstance.focus).toHaveBeenCalledTimes(1); }); it("createMainWindow fallback timer shows and focuses when ready-to-show never fires", async () => { vi.useFakeTimers(); const { createMainWindow } = await importMainModule(); createMainWindow(); vi.advanceTimersByTime(2000); expect(mocks.browserWindowInstance.show).toHaveBeenCalledTimes(1); expect(mocks.browserWindowInstance.focus).toHaveBeenCalledTimes(1); vi.useRealTimers(); }); it("initializeApp drops off-screen x/y before BrowserWindow construction", async () => { mainDeps.loadWindowState.mockResolvedValueOnce({ x: -9999, y: -9999, width: 1280, height: 900, isMaximized: false, }); const { initializeApp } = await importMainModule(); await initializeApp(); const [options] = mocks.BrowserWindow.mock.calls[0] as Array>; expect(options).not.toHaveProperty("x"); expect(options).not.toHaveProperty("y"); }); it("initializeApp preserves x/y when state overlaps a visible display", async () => { mainDeps.loadWindowState.mockResolvedValueOnce({ x: 100, y: 100, width: 1280, height: 900, isMaximized: false, }); const { initializeApp } = await importMainModule(); await initializeApp(); const [options] = mocks.BrowserWindow.mock.calls[0] as Array>; expect(options).toMatchObject({ x: 100, y: 100 }); }); it("importing main does not auto-start", async () => { await importMainModule(); expect(mocks.app.whenReady).not.toHaveBeenCalled(); }); it("exports run for app entrypoint wiring", async () => { const mainModule = await importMainModule(); expect(typeof mainModule.run).toBe("function"); }); });