feat(FN-1072): add Electron desktop renderer integration
- Add Electron main-process IPC handlers and preload bridges for API proxying, window controls, update install, and platform lookup - Introduce a desktop renderer entrypoint with DesktopWrapper and a custom frameless TitleBar component - Add Electron-aware renderer utilities including API transport selection and hooks for runtime detection, auto-update events, and deep links - Update dashboard header behavior for Electron mode and expand desktop tests across preload, transport, hooks, and title bar flows - Document the desktop renderer architecture and update desktop package config/dependencies
This commit is contained in:
@@ -24,14 +24,31 @@ 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;
|
||||
function getExposedFusionDesktopApi() {
|
||||
const call = mocks.contextBridge.exposeInMainWorld.mock.calls.find(
|
||||
(entry) => entry[0] === "fusionDesktop",
|
||||
) as [string, {
|
||||
getAppVersion: () => Promise<string>;
|
||||
quit: () => void;
|
||||
onDashboardReady: (callback: () => void) => () => void;
|
||||
}] | undefined;
|
||||
|
||||
return call?.[1];
|
||||
}
|
||||
|
||||
function getExposedElectronApi() {
|
||||
const call = mocks.contextBridge.exposeInMainWorld.mock.calls.find(
|
||||
(entry) => entry[0] === "electronAPI",
|
||||
) as [string, {
|
||||
invoke: (channel: string, payload?: unknown) => Promise<unknown>;
|
||||
apiRequest: (method: string, path: string, body?: unknown) => Promise<unknown>;
|
||||
getServerPort: () => Promise<number>;
|
||||
windowControl: (action: string) => Promise<boolean | void>;
|
||||
onUpdateAvailable: (callback: (info: Record<string, unknown>) => void) => () => void;
|
||||
installUpdate: () => Promise<void>;
|
||||
onDeepLink: (callback: (url: string) => void) => () => void;
|
||||
getPlatform: () => Promise<string>;
|
||||
}] | undefined;
|
||||
|
||||
return call?.[1];
|
||||
}
|
||||
@@ -42,39 +59,43 @@ describe("preload", () => {
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
it("contextBridge.exposeInMainWorld called with fusionDesktop", async () => {
|
||||
it("exposes fusionDesktop and electronAPI", async () => {
|
||||
await importPreloadModule();
|
||||
|
||||
expect(mocks.contextBridge.exposeInMainWorld).toHaveBeenCalledWith(
|
||||
"fusionDesktop",
|
||||
expect.any(Object),
|
||||
);
|
||||
expect(mocks.contextBridge.exposeInMainWorld).toHaveBeenCalledWith(
|
||||
"electronAPI",
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
|
||||
it("getAppVersion calls ipcRenderer.invoke", async () => {
|
||||
it("fusionDesktop.getAppVersion calls ipcRenderer.invoke", async () => {
|
||||
mocks.ipcRenderer.invoke.mockResolvedValue("0.1.0");
|
||||
await importPreloadModule();
|
||||
|
||||
const api = getExposedApi();
|
||||
const api = getExposedFusionDesktopApi();
|
||||
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 () => {
|
||||
it("fusionDesktop.quit calls ipcRenderer.send", async () => {
|
||||
await importPreloadModule();
|
||||
|
||||
const api = getExposedApi();
|
||||
const api = getExposedFusionDesktopApi();
|
||||
api?.quit();
|
||||
|
||||
expect(mocks.ipcRenderer.send).toHaveBeenCalledWith("app:quit");
|
||||
});
|
||||
|
||||
it("onDashboardReady returns unsubscribe function", async () => {
|
||||
it("fusionDesktop.onDashboardReady returns unsubscribe function", async () => {
|
||||
await importPreloadModule();
|
||||
|
||||
const api = getExposedApi();
|
||||
const api = getExposedFusionDesktopApi();
|
||||
const callback = vi.fn();
|
||||
const unsubscribe = api?.onDashboardReady(callback);
|
||||
|
||||
@@ -91,4 +112,43 @@ describe("preload", () => {
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it("electronAPI methods invoke expected IPC channels", async () => {
|
||||
await importPreloadModule();
|
||||
|
||||
const api = getExposedElectronApi();
|
||||
await api?.invoke("api-request", { method: "GET", path: "/tasks" });
|
||||
await api?.apiRequest("POST", "/tasks", { title: "Task" });
|
||||
await api?.getServerPort();
|
||||
await api?.windowControl("maximize");
|
||||
await api?.installUpdate();
|
||||
await api?.getPlatform();
|
||||
|
||||
expect(mocks.ipcRenderer.invoke).toHaveBeenCalledWith("api-request", { method: "GET", path: "/tasks" });
|
||||
expect(mocks.ipcRenderer.invoke).toHaveBeenCalledWith("api-request", { method: "POST", path: "/tasks", body: { title: "Task" } });
|
||||
expect(mocks.ipcRenderer.invoke).toHaveBeenCalledWith("server:get-port");
|
||||
expect(mocks.ipcRenderer.invoke).toHaveBeenCalledWith("window:control", "maximize");
|
||||
expect(mocks.ipcRenderer.invoke).toHaveBeenCalledWith("update:install");
|
||||
expect(mocks.ipcRenderer.invoke).toHaveBeenCalledWith("system:get-platform");
|
||||
});
|
||||
|
||||
it("electronAPI event subscriptions provide unsubscribe functions", async () => {
|
||||
await importPreloadModule();
|
||||
|
||||
const api = getExposedElectronApi();
|
||||
const onUpdate = vi.fn();
|
||||
const onDeepLink = vi.fn();
|
||||
|
||||
const unsubscribeUpdate = api?.onUpdateAvailable(onUpdate);
|
||||
const unsubscribeDeepLink = api?.onDeepLink(onDeepLink);
|
||||
|
||||
expect(mocks.ipcRenderer.on).toHaveBeenCalledWith("update:available", expect.any(Function));
|
||||
expect(mocks.ipcRenderer.on).toHaveBeenCalledWith("deep-link", expect.any(Function));
|
||||
|
||||
unsubscribeUpdate?.();
|
||||
unsubscribeDeepLink?.();
|
||||
|
||||
expect(mocks.ipcRenderer.removeListener).toHaveBeenCalledWith("update:available", expect.any(Function));
|
||||
expect(mocks.ipcRenderer.removeListener).toHaveBeenCalledWith("deep-link", expect.any(Function));
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user