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>
173 lines
6.1 KiB
TypeScript
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" });
|
|
});
|
|
});
|