Files
fusion/packages/desktop/src/__tests__/ipc.test.ts
Fusion 3df6dab652 docs(FN-3408): document shell connection chrome contracts
- Add architecture notes describing shell connection chrome contracts
- Expand dashboard README with shell chrome expectations and integration details
- Add desktop README note aligning desktop shell behavior with documented contracts

Fusion-Task-Id: FN-3408
2026-05-08 00:40:15 -07:00

216 lines
8.8 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
const mocks = vi.hoisted(() => {
const ipcHandlers = new Map<string, (...args: unknown[]) => unknown>();
const ipcMain = {
handle: vi.fn((channel: string, handler: (...args: unknown[]) => unknown) => {
ipcHandlers.set(channel, handler);
}),
};
const app = {
getVersion: vi.fn(() => "1.2.3"),
};
const updateTrayStatus = vi.fn();
const showExportSettingsDialog = vi.fn();
const showImportSettingsDialog = vi.fn();
const setupAutoUpdater = vi.fn();
const readShellSettings = vi.fn(async () => ({
desktopMode: "remote",
hasCompletedModeSelection: true,
activeProfileId: null,
profiles: [],
}));
const writeShellSettings = vi.fn(async () => undefined);
return {
ipcMain,
ipcHandlers,
app,
updateTrayStatus,
showExportSettingsDialog,
showImportSettingsDialog,
setupAutoUpdater,
readShellSettings,
writeShellSettings,
};
});
vi.mock("electron", () => ({
ipcMain: mocks.ipcMain,
app: mocks.app,
}));
vi.mock("../tray.js", () => ({
updateTrayStatus: mocks.updateTrayStatus,
}));
vi.mock("../native.js", () => ({
showExportSettingsDialog: mocks.showExportSettingsDialog,
showImportSettingsDialog: mocks.showImportSettingsDialog,
setupAutoUpdater: mocks.setupAutoUpdater,
}));
vi.mock("../shell-settings.js", () => ({
readShellSettings: mocks.readShellSettings,
writeShellSettings: mocks.writeShellSettings,
buildSavedProfile: (settings: { profiles: Array<{ id: string }>; }, profile: { id?: string; name: string; serverUrl: string }) => ({
id: profile.id ?? "generated-id",
name: profile.name.trim() || "Remote Server",
serverUrl: profile.serverUrl,
createdAt: "",
updatedAt: "",
authToken: null,
lastUsedAt: null,
}),
applyDeleteProfile: (settings: { activeProfileId: string | null; profiles: Array<{ id: string }> }, profileId: string) => {
const profiles = settings.profiles.filter((item) => item.id !== profileId);
return {
...settings,
profiles,
activeProfileId: settings.activeProfileId === profileId ? (profiles[0]?.id ?? null) : settings.activeProfileId,
};
},
applySetActiveProfile: (settings: { profiles: Array<{ id: string }> }, profileId: string | null) => ({
...settings,
activeProfileId: profileId && settings.profiles.some((item) => item.id === profileId) ? profileId : null,
}),
getDesktopShellModeState: (settings: { hasCompletedModeSelection?: boolean; desktopMode?: "local" | "remote" | null }) => ({
isFirstRun: !settings.hasCompletedModeSelection || !settings.desktopMode,
desktopMode: settings.desktopMode ?? null,
}),
}));
function createWindowMock() {
return {
minimize: vi.fn(),
maximize: vi.fn(),
unmaximize: vi.fn(),
close: vi.fn(),
isMaximized: vi.fn(() => false),
webContents: { send: vi.fn() },
};
}
function createTrayMock() {
return {
setToolTip: vi.fn(),
setContextMenu: vi.fn(),
};
}
async function registerHandlers(options: Record<string, unknown> = {}) {
const { registerIpcHandlers } = await import("../ipc.ts");
const window = createWindowMock();
const tray = createTrayMock();
registerIpcHandlers(window as never, tray as never, options as never);
return { window, tray };
}
describe("ipc handlers", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.resetModules();
mocks.ipcHandlers.clear();
});
it("registers shell channels", async () => {
await registerHandlers();
const channels = new Set(mocks.ipcMain.handle.mock.calls.map(([channel]) => channel));
expect(channels.has("shell:getState")).toBe(true);
expect(channels.has("shell:saveProfile")).toBe(true);
expect(channels.has("shell:getDesktopModeState")).toBe(true);
expect(channels.has("shell:setDesktopMode")).toBe(true);
expect(channels.has("desktopRuntime:getStatus")).toBe(true);
expect(channels.has("desktopRuntime:startLocal")).toBe(true);
expect(channels.has("desktopRuntime:stopLocal")).toBe(true);
expect(channels.has("desktopLaunchMode:getMode")).toBe(true);
expect(channels.has("desktopLaunchMode:getContext")).toBe(true);
expect(channels.has("desktopLaunchMode:setMode")).toBe(true);
expect(channels.has("platform:get")).toBe(true);
expect(channels.has("shell:openConnectionManager")).toBe(true);
});
it("shell:getState returns desktop shell state", async () => {
await registerHandlers({ getRuntimeStatus: () => ({ source: "none", state: "stopped" }) });
const handler = mocks.ipcHandlers.get("shell:getState");
const result = await handler?.({});
expect(result).toMatchObject({ host: "desktop-shell", desktopMode: "remote" });
expect(result).toMatchObject({ desktopModeState: { isFirstRun: false, desktopMode: "remote" } });
expect(result).toMatchObject({ localRuntime: { source: "none", state: "stopped" } });
});
it("shell:setDesktopMode persists mode and emits state", async () => {
const onDesktopModeChange = vi.fn(async () => undefined);
const { window } = await registerHandlers({ onDesktopModeChange, getRuntimeStatus: () => ({ source: "none", state: "stopped" }) });
const handler = mocks.ipcHandlers.get("shell:setDesktopMode");
await handler?.({}, "local");
expect(mocks.writeShellSettings).toHaveBeenCalledWith(
expect.objectContaining({ desktopMode: "local", hasCompletedModeSelection: true }),
);
expect(onDesktopModeChange).toHaveBeenCalledWith("local");
expect(window.webContents.send).toHaveBeenCalledWith("shell:state", expect.any(Object));
});
it("desktop launch mode handlers return mode/context and validate payload", async () => {
const getDesktopLaunchContext = vi.fn(() => ({ mode: "remote", profileId: "profile_1", serverBaseUrl: "https://remote.example.com" }));
const onDesktopLaunchModeChange = vi.fn(async () => undefined);
const getDesktopLaunchMode = vi.fn(() => "remote");
await registerHandlers({ onDesktopLaunchModeChange, getDesktopLaunchMode, getDesktopLaunchContext });
await expect(mocks.ipcHandlers.get("desktopLaunchMode:getMode")?.({})).resolves.toBe("remote");
await expect(mocks.ipcHandlers.get("desktopLaunchMode:getContext")?.({})).resolves.toEqual({ mode: "remote", profileId: "profile_1", serverBaseUrl: "https://remote.example.com" });
await expect(mocks.ipcHandlers.get("desktopLaunchMode:setMode")?.({}, "local")).resolves.toBe("remote");
await expect(mocks.ipcHandlers.get("desktopLaunchMode:setMode")?.({}, "bad")).rejects.toThrow("Invalid desktop launch mode");
expect(onDesktopLaunchModeChange).toHaveBeenCalledWith("local");
});
it("desktopRuntime start/stop/getStatus handlers proxy runtime manager", async () => {
const getRuntimeStatus = vi.fn(() => ({ source: "none", state: "stopped" }));
const startLocalRuntime = vi.fn(async () => ({ source: "embedded-local", state: "running", port: 4510 }));
const stopLocalRuntime = vi.fn(async () => ({ source: "embedded-local", state: "running", port: 9999 }));
await registerHandlers({ getRuntimeStatus, startLocalRuntime, stopLocalRuntime });
await expect(mocks.ipcHandlers.get("desktopRuntime:getStatus")?.({})).resolves.toEqual({ source: "none", state: "stopped" });
await expect(mocks.ipcHandlers.get("desktopRuntime:startLocal")?.({})).resolves.toEqual({ source: "embedded-local", state: "running", port: 4510 });
await expect(mocks.ipcHandlers.get("desktopRuntime:stopLocal")?.({})).resolves.toEqual({ source: "embedded-local", state: "running", port: 9999 });
});
it("shell:openConnectionManager notifies renderer", async () => {
const { window } = await registerHandlers();
const result = mocks.ipcHandlers.get("shell:openConnectionManager")?.({});
expect(result).toBeUndefined();
expect(window.webContents.send).toHaveBeenCalledWith("shell:open-connection-manager");
});
it("shell:saveProfile persists the helper-generated profile", async () => {
await registerHandlers();
const handler = mocks.ipcHandlers.get("shell:saveProfile");
const result = await handler?.({}, { name: " Prod ", serverUrl: "https://fusion.example.com" });
expect(result).toMatchObject({ id: "generated-id", name: "Prod" });
expect(mocks.writeShellSettings).toHaveBeenCalledWith(expect.objectContaining({ profiles: [expect.objectContaining({ id: "generated-id" })] }));
});
it("shell:deleteProfile falls back to first remaining profile when deleting active", async () => {
mocks.readShellSettings.mockResolvedValueOnce({
desktopMode: "remote",
hasCompletedModeSelection: true,
activeProfileId: "p2",
profiles: [{ id: "p1" }, { id: "p2" }],
});
await registerHandlers();
const handler = mocks.ipcHandlers.get("shell:deleteProfile");
await handler?.({}, "p2");
expect(mocks.writeShellSettings).toHaveBeenCalledWith(expect.objectContaining({ activeProfileId: "p1" }));
});
});