evOo-local/apps/desktop/src/preload.test.ts
cynarAI 49431def83
feat: show the bot's live cursor and make the in-app browser work (#113)
Bots now announce every click and typing step before it runs, and the
web app draws a cursor in the bot's colour that glides to that point
over the live computer screen, ripples on clicks and shows typing.

- contracts: ComputerPointerPayload on the existing thread.computer event
- executor: computer_act and browser_act announce pointer steps
  (best effort); the page-browser helper reports element screen boxes,
  which are stripped before results reach the model
- web: agentPointerFeed + AgentCursor over the side-panel preview and
  the full-window screen, mapped through the noVNC letterbox
- in-app browser: native WebContentsView on desktop (own session, http(s)
  only, clamped bounds, back/forward, reload, DevTools, load errors) and
  a sandboxed iframe on the web; replaces the dead panel, the fake
  50%/40% cursors and the unimplemented devServer bridge


Claude-Session: https://claude.ai/code/session_01WiGooXtsKrZQmdszepRrNi

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-03 19:08:48 +02:00

173 lines
6.1 KiB
TypeScript

import { readFileSync } from "node:fs";
import path from "node:path";
import vm from "node:vm";
import type { RakazoDesktop, RakazoSetup } from "@rakazo/contracts";
import { describe, expect, it, vi } from "vitest";
function runPreload(file: string, ipc: { invoke?: unknown; on?: unknown; off?: unknown } = {}) {
const invoke =
(ipc.invoke as ReturnType<typeof vi.fn>) ?? vi.fn(async (channel: string) => ({ channel }));
const on = (ipc.on as ReturnType<typeof vi.fn>) ?? vi.fn();
const off = (ipc.off as ReturnType<typeof vi.fn>) ?? vi.fn();
const exposeInMainWorld = vi.fn();
const source = readFileSync(path.join(import.meta.dirname, file), "utf8");
vm.runInNewContext(source, {
process: { platform: "linux" },
require(moduleName: string) {
if (moduleName !== "electron") throw new Error(`Unexpected preload import: ${moduleName}`);
return { contextBridge: { exposeInMainWorld }, ipcRenderer: { invoke, on, off } };
},
});
return { invoke, on, off, exposeInMainWorld };
}
describe("desktop preload bridge", () => {
it("exposes the scoped desktop bridges", async () => {
const { invoke, exposeInMainWorld } = runPreload("preload.cjs");
expect(exposeInMainWorld).toHaveBeenCalledTimes(1);
const [globalName, bridge] = exposeInMainWorld.mock.calls[0] as [string, RakazoDesktop];
expect(globalName).toBe("rakazoDesktop");
expect(bridge.platform).toBe("linux");
expect(Object.keys(bridge).sort()).toEqual([
"browserPreview",
"files",
"localSettings",
"oauth",
"platform",
"update",
"window",
]);
expect(Object.keys(bridge.window).sort()).toEqual([
"close",
"minimize",
"state",
"toggleMaximize",
]);
expect(Object.keys(bridge.update).sort()).toEqual(["check", "download", "install", "state"]);
expect(Object.keys(bridge.browserPreview ?? {}).sort()).toEqual([
"back",
"forward",
"hide",
"navigate",
"onState",
"openDevTools",
"reload",
"show",
"state",
"stop",
]);
await bridge.files?.pickDirectory();
await bridge.oauth.open?.("https://provider.example.com/authorize");
await bridge.oauth.cancel?.("https://provider.example.com/authorize");
await bridge.window.close();
await bridge.window.minimize();
await bridge.window.toggleMaximize();
await bridge.window.state();
await bridge.update.state();
await bridge.update.check();
await bridge.update.download();
await bridge.update.install();
await bridge.browserPreview?.navigate("http://localhost:5173/");
await bridge.browserPreview?.show({ x: 1, y: 2, width: 3, height: 4 });
expect(invoke.mock.calls.map(([channel]) => channel)).toEqual([
"desktop.files.pickDirectory",
"desktop.oauth.open",
"desktop.oauth.cancel",
"desktop.window.close",
"desktop.window.minimize",
"desktop.window.toggleMaximize",
"desktop.window.state",
"desktop.update.state",
"desktop.update.check",
"desktop.update.download",
"desktop.update.install",
"desktop.browserPreview.navigate",
"desktop.browserPreview.show",
]);
});
it("keeps setup off the app bridge so a connected server cannot re-point the app", () => {
const { exposeInMainWorld } = runPreload("preload.cjs");
const [, bridge] = exposeInMainWorld.mock.calls[0] as [string, Record<string, unknown>];
expect(Object.keys(bridge).sort()).toEqual([
"browserPreview",
"files",
"localSettings",
"oauth",
"platform",
"update",
"window",
]);
});
it("forwards captured codes without leaking the IPC event to the renderer", () => {
const listeners: Array<(event: unknown, callback: unknown) => void> = [];
const on = vi.fn((_channel: string, handler: (event: unknown, callback: unknown) => void) => {
listeners.push(handler);
});
const off = vi.fn();
const { exposeInMainWorld } = runPreload("preload.cjs", { on, off });
const [, bridge] = exposeInMainWorld.mock.calls[0] as [string, RakazoDesktop];
const received: unknown[] = [];
const unsubscribe = bridge.oauth.onCallback((callback) => received.push(callback));
expect(on).toHaveBeenCalledWith("desktop.oauth.callback", expect.any(Function));
listeners[0]?.({ sender: "ipc-event" }, { code: "ac_123", state: "verifier_456" });
expect(received).toEqual([{ code: "ac_123", state: "verifier_456" }]);
unsubscribe();
expect(off).toHaveBeenCalledWith("desktop.oauth.callback", expect.any(Function));
});
});
describe("setup preload bridge", () => {
it("exposes only the first-run setup operations", async () => {
const { invoke, on, exposeInMainWorld } = runPreload("setup-preload.cjs");
expect(exposeInMainWorld).toHaveBeenCalledTimes(1);
const [globalName, bridge] = exposeInMainWorld.mock.calls[0] as [string, RakazoSetup];
expect(globalName).toBe("rakazoSetup");
expect(bridge.platform).toBe("linux");
expect(Object.keys(bridge).sort()).toEqual([
"openLink",
"platform",
"quit",
"save",
"stack",
"state",
"test",
]);
expect(Object.keys(bridge.stack).sort()).toEqual(["onChange", "start", "state"]);
await bridge.state();
await bridge.test("http://127.0.0.1:5173");
await bridge.save({ mode: "new", serverUrl: "http://127.0.0.1:5173" });
await bridge.quit();
await bridge.openLink("orbstack");
await bridge.stack.state();
await bridge.stack.start();
expect(invoke.mock.calls.map(([channel]) => channel)).toEqual([
"desktop.setup.state",
"desktop.setup.test",
"desktop.setup.save",
"desktop.setup.quit",
"desktop.setup.openLink",
"desktop.setup.stack.state",
"desktop.setup.stack.start",
]);
expect(invoke).toHaveBeenCalledWith("desktop.setup.openLink", "orbstack");
const listener = vi.fn();
bridge.stack.onChange(listener);
const [channel, handler] = on.mock.calls.at(-1) as [string, (...args: unknown[]) => void];
expect(channel).toBe("desktop.setup.stack.changed");
handler({}, { phase: "pulling" });
expect(listener).toHaveBeenCalledWith({ phase: "pulling" });
});
});