refactor(browser): prune browser automation surface
This commit is contained in:
@@ -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: {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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).
|
||||
}
|
||||
|
||||
@@ -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() : "";
|
||||
|
||||
Reference in New Issue
Block a user