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>
97 lines
3.3 KiB
TypeScript
97 lines
3.3 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
previewBounds,
|
|
previewLoadError,
|
|
previewShortcut,
|
|
previewUrl,
|
|
} from "./browser-preview-policy.js";
|
|
|
|
describe("previewUrl", () => {
|
|
it("accepts only http(s) without credentials", () => {
|
|
expect(previewUrl("http://localhost:5173")).toBe("http://localhost:5173/");
|
|
expect(previewUrl("https://example.com/a?b=1")).toBe("https://example.com/a?b=1");
|
|
for (const value of [
|
|
"file:///etc/hosts",
|
|
"javascript:alert(1)",
|
|
"chrome://settings",
|
|
"https://user:fake-password@example.com",
|
|
"not a url",
|
|
42,
|
|
`https://example.com/${"a".repeat(9000)}`,
|
|
]) {
|
|
expect(previewUrl(value)).toBeNull();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("previewBounds", () => {
|
|
const content = { width: 1200, height: 800 };
|
|
|
|
it("rounds and applies the app zoom factor", () => {
|
|
expect(previewBounds({ x: 800.4, y: 40.6, width: 399.5, height: 700 }, content)).toEqual({
|
|
x: 800,
|
|
y: 41,
|
|
width: 400,
|
|
height: 700,
|
|
});
|
|
expect(previewBounds({ x: 100, y: 10, width: 200, height: 100 }, content, 1.25)).toEqual({
|
|
x: 125,
|
|
y: 13,
|
|
width: 250,
|
|
height: 125,
|
|
});
|
|
});
|
|
|
|
it("clamps to the window and rejects empty or malformed rectangles", () => {
|
|
expect(previewBounds({ x: -50, y: -10, width: 5000, height: 5000 }, content)).toEqual({
|
|
x: 0,
|
|
y: 0,
|
|
width: 1200,
|
|
height: 800,
|
|
});
|
|
expect(previewBounds({ x: 1300, y: 0, width: 100, height: 100 }, content)).toBeNull();
|
|
expect(previewBounds({ x: 0, y: 0, width: 0, height: 100 }, content)).toBeNull();
|
|
expect(previewBounds({ x: "1", y: 0, width: 1, height: 1 }, content)).toBeNull();
|
|
expect(previewBounds({ x: Number.NaN, y: 0, width: 1, height: 1 }, content)).toBeNull();
|
|
expect(previewBounds(null, content)).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("previewShortcut", () => {
|
|
const key = (overrides: Partial<Parameters<typeof previewShortcut>[0]>) => ({
|
|
type: "keyDown",
|
|
key: "r",
|
|
control: false,
|
|
meta: false,
|
|
alt: false,
|
|
shift: false,
|
|
...overrides,
|
|
});
|
|
|
|
it("maps platform browser shortcuts", () => {
|
|
expect(previewShortcut(key({ meta: true }), "darwin")).toBe("reload");
|
|
expect(previewShortcut(key({ meta: true, shift: true, key: "R" }), "darwin")).toBe(
|
|
"hard-reload",
|
|
);
|
|
expect(previewShortcut(key({ control: true }), "linux")).toBe("reload");
|
|
expect(previewShortcut(key({ meta: true, key: "[" }), "darwin")).toBe("back");
|
|
expect(previewShortcut(key({ alt: true, key: "ArrowRight" }), "win32")).toBe("forward");
|
|
expect(previewShortcut(key({ key: "F12" }), "linux")).toBe("devtools");
|
|
expect(previewShortcut(key({ key: "F5" }), "win32")).toBe("reload");
|
|
});
|
|
|
|
it("leaves typing alone", () => {
|
|
expect(previewShortcut(key({}), "darwin")).toBeNull();
|
|
expect(previewShortcut(key({ control: true }), "darwin")).toBeNull();
|
|
expect(previewShortcut(key({ type: "keyUp", meta: true }), "darwin")).toBeNull();
|
|
expect(previewShortcut(key({ key: "ArrowLeft" }), "linux")).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("previewLoadError", () => {
|
|
it("ignores superseded navigations", () => {
|
|
expect(previewLoadError(-3, "ERR_ABORTED")).toBeNull();
|
|
expect(previewLoadError(-102, "ERR_CONNECTION_REFUSED")).toBe("ERR_CONNECTION_REFUSED");
|
|
});
|
|
});
|