refactor(browser): prune browser automation surface

This commit is contained in:
Peter Steinberger
2025-12-20 02:53:22 +00:00
parent 849446ae17
commit 6fc30962d6
19 changed files with 85 additions and 802 deletions

View File

@@ -3,23 +3,19 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
import type { BrowserRouteContext } from "../server-context.js";
const pw = vi.hoisted(() => ({
armDialogViaPlaywright: vi.fn().mockResolvedValue(undefined),
armFileUploadViaPlaywright: vi.fn().mockResolvedValue(undefined),
clickViaPlaywright: vi.fn().mockResolvedValue(undefined),
closePageViaPlaywright: vi.fn().mockResolvedValue(undefined),
dragViaPlaywright: vi.fn().mockResolvedValue(undefined),
evaluateViaPlaywright: vi.fn().mockResolvedValue("result"),
fileUploadViaPlaywright: vi.fn().mockResolvedValue(undefined),
fillFormViaPlaywright: vi.fn().mockResolvedValue(undefined),
handleDialogViaPlaywright: vi
.fn()
.mockResolvedValue({ message: "ok", type: "alert" }),
hoverViaPlaywright: vi.fn().mockResolvedValue(undefined),
navigateBackViaPlaywright: vi.fn().mockResolvedValue({ url: "about:blank" }),
navigateViaPlaywright: vi
.fn()
.mockResolvedValue({ url: "https://example.com" }),
pressKeyViaPlaywright: vi.fn().mockResolvedValue(undefined),
resizeViewportViaPlaywright: vi.fn().mockResolvedValue(undefined),
runCodeViaPlaywright: vi.fn().mockResolvedValue("ok"),
selectOptionViaPlaywright: vi.fn().mockResolvedValue(undefined),
typeViaPlaywright: vi.fn().mockResolvedValue(undefined),
waitForViaPlaywright: vi.fn().mockResolvedValue(undefined),
@@ -127,14 +123,14 @@ describe("handleBrowserActionCore", () => {
{
action: "dialog" as const,
args: { accept: true, promptText: "ok" },
fn: pw.handleDialogViaPlaywright,
fn: pw.armDialogViaPlaywright,
expectArgs: {
cdpPort: 18792,
targetId: "tab1",
accept: true,
promptText: "ok",
},
expectBody: { ok: true, message: "ok", type: "alert" },
expectBody: { ok: true },
},
{
action: "evaluate" as const,
@@ -151,7 +147,7 @@ describe("handleBrowserActionCore", () => {
{
action: "upload" as const,
args: { paths: ["/tmp/file.txt"] },
fn: pw.fileUploadViaPlaywright,
fn: pw.armFileUploadViaPlaywright,
expectArgs: {
cdpPort: 18792,
targetId: "tab1",
@@ -202,20 +198,6 @@ describe("handleBrowserActionCore", () => {
},
expectBody: { ok: true, targetId: "tab1", url: baseTab.url },
},
{
action: "back" as const,
args: {},
fn: pw.navigateBackViaPlaywright,
expectArgs: { cdpPort: 18792, targetId: "tab1" },
expectBody: { ok: true, targetId: "tab1", url: "about:blank" },
},
{
action: "run" as const,
args: { code: "return 1" },
fn: pw.runCodeViaPlaywright,
expectArgs: { cdpPort: 18792, targetId: "tab1", code: "return 1" },
expectBody: { ok: true, result: "ok" },
},
{
action: "click" as const,
args: {

View File

@@ -1,19 +1,17 @@
import type express from "express";
import {
armDialogViaPlaywright,
armFileUploadViaPlaywright,
clickViaPlaywright,
closePageViaPlaywright,
dragViaPlaywright,
evaluateViaPlaywright,
fileUploadViaPlaywright,
fillFormViaPlaywright,
handleDialogViaPlaywright,
hoverViaPlaywright,
navigateBackViaPlaywright,
navigateViaPlaywright,
pressKeyViaPlaywright,
resizeViewportViaPlaywright,
runCodeViaPlaywright,
selectOptionViaPlaywright,
typeViaPlaywright,
waitForViaPlaywright,
@@ -51,7 +49,6 @@ function normalizeModifiers(value: unknown): KeyboardModifier[] | undefined {
}
export type BrowserActionCore =
| "back"
| "click"
| "close"
| "dialog"
@@ -62,7 +59,6 @@ export type BrowserActionCore =
| "navigate"
| "press"
| "resize"
| "run"
| "select"
| "type"
| "upload"
@@ -115,13 +111,13 @@ export async function handleBrowserActionCore(
}
const promptText = toStringOrEmpty(args.promptText) || undefined;
const tab = await ctx.ensureTabAvailable(target);
const result = await handleDialogViaPlaywright({
await armDialogViaPlaywright({
cdpPort,
targetId: tab.targetId,
accept,
promptText,
});
res.json({ ok: true, ...result });
res.json({ ok: true });
return true;
}
case "evaluate": {
@@ -144,7 +140,7 @@ export async function handleBrowserActionCore(
case "upload": {
const paths = toStringArray(args.paths) ?? [];
const tab = await ctx.ensureTabAvailable(target);
await fileUploadViaPlaywright({
await armFileUploadViaPlaywright({
cdpPort,
targetId: tab.targetId,
paths: paths.length ? paths : undefined,
@@ -220,30 +216,6 @@ export async function handleBrowserActionCore(
res.json({ ok: true, targetId: tab.targetId, ...result });
return true;
}
case "back": {
const tab = await ctx.ensureTabAvailable(target);
const result = await navigateBackViaPlaywright({
cdpPort,
targetId: tab.targetId,
});
res.json({ ok: true, targetId: tab.targetId, ...result });
return true;
}
case "run": {
const code = toStringOrEmpty(args.code);
if (!code) {
jsonError(res, 400, "code is required");
return true;
}
const tab = await ctx.ensureTabAvailable(target);
const result = await runCodeViaPlaywright({
cdpPort,
targetId: tab.targetId,
code,
});
res.json({ ok: true, result });
return true;
}
case "click": {
const ref = toStringOrEmpty(args.ref);
if (!ref) {

View File

@@ -4,9 +4,6 @@ import type { BrowserRouteContext } from "../server-context.js";
const pw = vi.hoisted(() => ({
getConsoleMessagesViaPlaywright: vi.fn().mockResolvedValue([]),
mouseClickViaPlaywright: vi.fn().mockResolvedValue(undefined),
mouseDragViaPlaywright: vi.fn().mockResolvedValue(undefined),
mouseMoveViaPlaywright: vi.fn().mockResolvedValue(undefined),
pdfViaPlaywright: vi.fn().mockResolvedValue({ buffer: Buffer.from("pdf") }),
verifyElementVisibleViaPlaywright: vi.fn().mockResolvedValue(undefined),
verifyListVisibleViaPlaywright: vi.fn().mockResolvedValue(undefined),
@@ -149,40 +146,6 @@ describe("handleBrowserActionExtra", () => {
},
expectBody: { ok: true },
},
{
action: "mouseMove" as const,
args: { x: 10, y: 20 },
fn: pw.mouseMoveViaPlaywright,
expectArgs: { cdpPort: 18792, targetId: "tab1", x: 10, y: 20 },
expectBody: { ok: true },
},
{
action: "mouseClick" as const,
args: { x: 1, y: 2, button: "right" },
fn: pw.mouseClickViaPlaywright,
expectArgs: {
cdpPort: 18792,
targetId: "tab1",
x: 1,
y: 2,
button: "right",
},
expectBody: { ok: true },
},
{
action: "mouseDrag" as const,
args: { startX: 1, startY: 2, endX: 3, endY: 4 },
fn: pw.mouseDragViaPlaywright,
expectArgs: {
cdpPort: 18792,
targetId: "tab1",
startX: 1,
startY: 2,
endX: 3,
endY: 4,
},
expectBody: { ok: true },
},
];
for (const item of cases) {

View File

@@ -5,9 +5,6 @@ import type express from "express";
import { ensureMediaDir, saveMediaBuffer } from "../../media/store.js";
import {
getConsoleMessagesViaPlaywright,
mouseClickViaPlaywright,
mouseDragViaPlaywright,
mouseMoveViaPlaywright,
pdfViaPlaywright,
verifyElementVisibleViaPlaywright,
verifyListVisibleViaPlaywright,
@@ -15,26 +12,10 @@ import {
verifyValueViaPlaywright,
} from "../pw-ai.js";
import type { BrowserRouteContext } from "../server-context.js";
import {
jsonError,
toNumber,
toStringArray,
toStringOrEmpty,
} from "./utils.js";
type MouseButton = "left" | "right" | "middle";
function normalizeMouseButton(value: unknown): MouseButton | undefined {
const raw = toStringOrEmpty(value);
if (raw === "left" || raw === "right" || raw === "middle") return raw;
return undefined;
}
import { jsonError, toStringArray, toStringOrEmpty } from "./utils.js";
export type BrowserActionExtra =
| "console"
| "mouseClick"
| "mouseDrag"
| "mouseMove"
| "pdf"
| "verifyElement"
| "verifyList"
@@ -157,68 +138,6 @@ export async function handleBrowserActionExtra(
res.json({ ok: true });
return true;
}
case "mouseMove": {
const x = toNumber(args.x);
const y = toNumber(args.y);
if (x === undefined || y === undefined) {
jsonError(res, 400, "x and y are required");
return true;
}
const tab = await ctx.ensureTabAvailable(target);
await mouseMoveViaPlaywright({
cdpPort,
targetId: tab.targetId,
x,
y,
});
res.json({ ok: true });
return true;
}
case "mouseClick": {
const x = toNumber(args.x);
const y = toNumber(args.y);
if (x === undefined || y === undefined) {
jsonError(res, 400, "x and y are required");
return true;
}
const button = normalizeMouseButton(args.button);
const tab = await ctx.ensureTabAvailable(target);
await mouseClickViaPlaywright({
cdpPort,
targetId: tab.targetId,
x,
y,
button,
});
res.json({ ok: true });
return true;
}
case "mouseDrag": {
const startX = toNumber(args.startX);
const startY = toNumber(args.startY);
const endX = toNumber(args.endX);
const endY = toNumber(args.endY);
if (
startX === undefined ||
startY === undefined ||
endX === undefined ||
endY === undefined
) {
jsonError(res, 400, "startX, startY, endX, endY are required");
return true;
}
const tab = await ctx.ensureTabAvailable(target);
await mouseDragViaPlaywright({
cdpPort,
targetId: tab.targetId,
startX,
startY,
endX,
endY,
});
res.json({ ok: true });
return true;
}
default:
return false;
}

View File

@@ -79,12 +79,6 @@ export function registerBrowserActionRoutes(
await runCoreAction(ctx, res, "navigate", body, targetId);
});
app.post("/back", async (req, res) => {
const body = readBody(req);
const targetId = readTargetId(body.targetId);
await runCoreAction(ctx, res, "back", body, targetId);
});
app.post("/resize", async (req, res) => {
const body = readBody(req);
const targetId = readTargetId(body.targetId);
@@ -163,12 +157,6 @@ export function registerBrowserActionRoutes(
await runCoreAction(ctx, res, "evaluate", body, targetId);
});
app.post("/run", async (req, res) => {
const body = readBody(req);
const targetId = readTargetId(body.targetId);
await runCoreAction(ctx, res, "run", body, targetId);
});
app.get("/console", async (req, res) => {
const targetId = readTargetId(req.query.targetId);
const level = toStringOrEmpty(req.query.level);
@@ -206,21 +194,5 @@ export function registerBrowserActionRoutes(
await runExtraAction(ctx, res, "verifyValue", body, targetId);
});
app.post("/mouse/move", async (req, res) => {
const body = readBody(req);
const targetId = readTargetId(body.targetId);
await runExtraAction(ctx, res, "mouseMove", body, targetId);
});
app.post("/mouse/click", async (req, res) => {
const body = readBody(req);
const targetId = readTargetId(body.targetId);
await runExtraAction(ctx, res, "mouseClick", body, targetId);
});
app.post("/mouse/drag", async (req, res) => {
const body = readBody(req);
const targetId = readTargetId(body.targetId);
await runExtraAction(ctx, res, "mouseDrag", body, targetId);
});
// Intentionally no coordinate-based mouse actions (move/click/drag).
}

View File

@@ -5,7 +5,6 @@ import { ensureMediaDir, saveMediaBuffer } from "../../media/store.js";
import {
captureScreenshot,
captureScreenshotPng,
evaluateJavaScript,
getDomText,
querySelector,
snapshotAria,
@@ -124,45 +123,6 @@ export function registerBrowserInspectRoutes(
}
});
app.post("/eval", async (req, res) => {
const js = toStringOrEmpty((req.body as { js?: unknown })?.js);
const targetId = toStringOrEmpty(
(req.body as { targetId?: unknown })?.targetId,
);
const awaitPromise = Boolean((req.body as { await?: unknown })?.await);
if (!js) return jsonError(res, 400, "js is required");
try {
const tab = await ctx.ensureTabAvailable(targetId || undefined);
const evaluated = await evaluateJavaScript({
wsUrl: tab.wsUrl ?? "",
expression: js,
awaitPromise,
returnByValue: true,
});
if (evaluated.exceptionDetails) {
const msg =
evaluated.exceptionDetails.exception?.description ||
evaluated.exceptionDetails.text ||
"JavaScript evaluation failed";
return jsonError(res, 400, msg);
}
res.json({
ok: true,
targetId: tab.targetId,
url: tab.url,
result: evaluated.result,
});
} catch (err) {
const mapped = ctx.mapTabError(err);
if (mapped) return jsonError(res, mapped.status, mapped.message);
jsonError(res, 500, String(err));
}
});
app.get("/query", async (req, res) => {
const selector =
typeof req.query.selector === "string" ? req.query.selector.trim() : "";