feat(FN-1070): restore desktop Electron scaffolding

- Reintroduce the @fusion/desktop workspace package with Electron main, preload, and renderer entrypoints
- Add desktop build/test tooling (TypeScript, Vitest, electron-builder) and update workspace build dependency settings for Electron
- Wire secure IPC bridge APIs for app version/quit flows with exported preload typings
- Add README usage docs and unit tests covering main-process window/tray behavior and preload API wiring
This commit is contained in:
gsxdsm
2026-04-07 22:33:05 -07:00
parent 57ff16704a
commit 4e41eb0e7a
13 changed files with 2230 additions and 42 deletions

View File

@@ -0,0 +1,94 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
const mocks = vi.hoisted(() => {
const contextBridge = {
exposeInMainWorld: vi.fn(),
};
const ipcRenderer = {
invoke: vi.fn(),
send: vi.fn(),
on: vi.fn(),
removeListener: vi.fn(),
};
return { contextBridge, ipcRenderer };
});
vi.mock("electron", () => ({
contextBridge: mocks.contextBridge,
ipcRenderer: mocks.ipcRenderer,
}));
async function importPreloadModule() {
await import("../preload.ts");
}
function getExposedApi() {
const call = mocks.contextBridge.exposeInMainWorld.mock.calls[0] as
| [string, {
getAppVersion: () => Promise<string>;
quit: () => void;
onDashboardReady: (callback: () => void) => () => void;
}]
| undefined;
return call?.[1];
}
describe("preload", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.resetModules();
});
it("contextBridge.exposeInMainWorld called with fusionDesktop", async () => {
await importPreloadModule();
expect(mocks.contextBridge.exposeInMainWorld).toHaveBeenCalledWith(
"fusionDesktop",
expect.any(Object),
);
});
it("getAppVersion calls ipcRenderer.invoke", async () => {
mocks.ipcRenderer.invoke.mockResolvedValue("0.1.0");
await importPreloadModule();
const api = getExposedApi();
const version = await api?.getAppVersion();
expect(version).toBe("0.1.0");
expect(mocks.ipcRenderer.invoke).toHaveBeenCalledWith("app:get-version");
});
it("quit calls ipcRenderer.send", async () => {
await importPreloadModule();
const api = getExposedApi();
api?.quit();
expect(mocks.ipcRenderer.send).toHaveBeenCalledWith("app:quit");
});
it("onDashboardReady returns unsubscribe function", async () => {
await importPreloadModule();
const api = getExposedApi();
const callback = vi.fn();
const unsubscribe = api?.onDashboardReady(callback);
expect(mocks.ipcRenderer.on).toHaveBeenCalledWith(
"dashboard:ready",
expect.any(Function),
);
expect(typeof unsubscribe).toBe("function");
unsubscribe?.();
expect(mocks.ipcRenderer.removeListener).toHaveBeenCalledWith(
"dashboard:ready",
expect.any(Function),
);
});
});