diff --git a/src/browser/pw-tools-interactions.ts b/src/browser/pw-tools-interactions.ts index e880f8e..5732aee 100644 --- a/src/browser/pw-tools-interactions.ts +++ b/src/browser/pw-tools-interactions.ts @@ -71,6 +71,33 @@ export async function clickViaPlaywright(opts: { } } +export async function clickAtViaPlaywright(opts: { + cdpUrl: string; + targetId?: string; + x: number; + y: number; + doubleClick?: boolean; + button?: "left" | "right" | "middle"; +}): Promise { + if (!Number.isFinite(opts.x) || !Number.isFinite(opts.y)) { + throw new Error("x and y must be finite numbers"); + } + const page = await getPageForTargetId({ + cdpUrl: opts.cdpUrl, + targetId: opts.targetId, + }); + ensurePageState(page); + if (opts.doubleClick) { + await page.mouse.dblclick(opts.x, opts.y, { + button: opts.button, + }); + } else { + await page.mouse.click(opts.x, opts.y, { + button: opts.button, + }); + } +} + export async function hoverViaPlaywright(opts: { cdpUrl: string; targetId?: string; diff --git a/src/mcp/tools/actions.ts b/src/mcp/tools/actions.ts index 088efd5..0e9b4e3 100644 --- a/src/mcp/tools/actions.ts +++ b/src/mcp/tools/actions.ts @@ -6,6 +6,7 @@ import type { ServerConfig } from "../../config.js"; import type { RegisterToolFn } from "../types.js"; import { clickViaPlaywright, + clickAtViaPlaywright, typeViaPlaywright, hoverViaPlaywright, pressKeyViaPlaywright, @@ -61,6 +62,59 @@ export function registerBrowserActionTools( } ); + // browser_click_at + register( + "browser_click_at", + "Click at absolute page coordinates (x, y). Use as last resort when browser_click (ref-based) and browser_evaluate (JS `element.click()`) both fail — e.g., canvas-rendered UI, invisible overlays, pointer-events traps. Get coordinates from browser_screenshot. Does NOT work inside cross-origin iframes — use browser_press_key keyboard navigation there.", + { + type: "object", + properties: { + x: { + type: "number", + description: "Absolute X coordinate in viewport pixels (from browser_screenshot)", + }, + y: { + type: "number", + description: "Absolute Y coordinate in viewport pixels", + }, + targetId: { + type: "string", + description: "Target ID of the tab", + }, + button: { + type: "string", + enum: ["left", "right", "middle"], + description: "Mouse button to click (default: left)", + }, + doubleClick: { + type: "boolean", + description: "Perform a double-click", + }, + }, + required: ["x", "y"], + }, + async (args: { + x: number; + y: number; + targetId?: string; + button?: "left" | "right" | "middle"; + doubleClick?: boolean; + }) => { + if (!config.cdpEndpoint) throw new Error("CDP endpoint not configured"); + + await clickAtViaPlaywright({ + cdpUrl: config.cdpEndpoint, + targetId: args.targetId, + x: args.x, + y: args.y, + button: args.button, + doubleClick: args.doubleClick, + }); + + return `**Clicked** at (${args.x}, ${args.y})`; + } + ); + // browser_type register( "browser_type", diff --git a/tests/click-at.test.ts b/tests/click-at.test.ts new file mode 100644 index 0000000..f26288e --- /dev/null +++ b/tests/click-at.test.ts @@ -0,0 +1,40 @@ +/** + * Unit tests for clickAtViaPlaywright's input validation. + * End-to-end behavior (actual mouse clicks) is only verifiable against a + * running CDP endpoint and is covered via manual smoke-testing in the PR. + */ + +import { describe, it, expect } from "vitest"; +import { clickAtViaPlaywright } from "../src/browser/pw-tools-interactions.js"; + +describe("clickAtViaPlaywright — input validation", () => { + it("rejects non-finite x", async () => { + await expect( + clickAtViaPlaywright({ + cdpUrl: "http://localhost:9222", + x: NaN, + y: 100, + }) + ).rejects.toThrow(/finite numbers/); + }); + + it("rejects non-finite y", async () => { + await expect( + clickAtViaPlaywright({ + cdpUrl: "http://localhost:9222", + x: 100, + y: Infinity, + }) + ).rejects.toThrow(/finite numbers/); + }); + + it("rejects missing coordinates", async () => { + await expect( + clickAtViaPlaywright({ + cdpUrl: "http://localhost:9222", + x: undefined as unknown as number, + y: 100, + }) + ).rejects.toThrow(/finite numbers/); + }); +});