refactor: lint cleanups and helpers

This commit is contained in:
Peter Steinberger
2025-12-23 00:28:40 +00:00
parent f5837dff9c
commit 918cbdcf03
39 changed files with 679 additions and 338 deletions

View File

@@ -2,7 +2,7 @@ import { createServer } from "node:http";
import { afterEach, describe, expect, it } from "vitest";
import { WebSocketServer } from "ws";
import { rawDataToString } from "../infra/ws.js";
import { createTargetViaCdp, evaluateJavaScript, snapshotAria } from "./cdp.js";
describe("cdp", () => {
@@ -29,7 +29,7 @@ describe("cdp", () => {
wsServer.on("connection", (socket) => {
socket.on("message", (data) => {
const msg = JSON.parse(String(data)) as {
const msg = JSON.parse(rawDataToString(data)) as {
id?: number;
method?: string;
params?: { url?: string };
@@ -78,7 +78,7 @@ describe("cdp", () => {
wsServer.on("connection", (socket) => {
socket.on("message", (data) => {
const msg = JSON.parse(String(data)) as {
const msg = JSON.parse(rawDataToString(data)) as {
id?: number;
method?: string;
params?: { expression?: string };
@@ -115,7 +115,7 @@ describe("cdp", () => {
wsServer.on("connection", (socket) => {
socket.on("message", (data) => {
const msg = JSON.parse(String(data)) as {
const msg = JSON.parse(rawDataToString(data)) as {
id?: number;
method?: string;
};

View File

@@ -1,5 +1,7 @@
import WebSocket from "ws";
import { rawDataToString } from "../infra/ws.js";
type CdpResponse = {
id: number;
result?: unknown;
@@ -44,7 +46,7 @@ function createCdpSender(ws: WebSocket) {
ws.on("message", (data) => {
try {
const parsed = JSON.parse(String(data)) as CdpResponse;
const parsed = JSON.parse(rawDataToString(data)) as CdpResponse;
if (typeof parsed.id !== "number") return;
const p = pending.get(parsed.id);
if (!p) return;
@@ -252,7 +254,11 @@ type RawAXNode = {
function axValue(v: unknown): string {
if (!v || typeof v !== "object") return "";
const value = (v as { value?: unknown }).value;
return typeof value === "string" ? value : String(value ?? "");
if (typeof value === "string") return value;
if (typeof value === "number" || typeof value === "boolean") {
return String(value);
}
return "";
}
function formatAriaSnapshot(
@@ -444,7 +450,13 @@ export async function getDomText(opts: {
awaitPromise: true,
returnByValue: true,
});
const text = String(evaluated.result?.value ?? "");
const textValue = (evaluated.result?.value ?? "") as unknown;
const text =
typeof textValue === "string"
? textValue
: typeof textValue === "number" || typeof textValue === "boolean"
? String(textValue)
: "";
return { text };
}

View File

@@ -5,6 +5,12 @@ import type {
} from "./client-actions-types.js";
import { fetchBrowserJson } from "./client-fetch.js";
export type BrowserFormField = {
ref: string;
type: string;
value?: string | number | boolean;
};
export type BrowserActRequest =
| {
kind: "click";
@@ -28,7 +34,7 @@ export type BrowserActRequest =
| { kind: "select"; ref: string; values: string[]; targetId?: string }
| {
kind: "fill";
fields: Array<Record<string, unknown>>;
fields: BrowserFormField[];
targetId?: string;
}
| { kind: "resize"; width: number; height: number; targetId?: string }

View File

@@ -1,3 +1,5 @@
import { extractErrorCode, formatErrorMessage } from "../infra/errors.js";
function unwrapCause(err: unknown): unknown {
if (!err || typeof err !== "object") return null;
const cause = (err as { cause?: unknown }).cause;
@@ -10,13 +12,7 @@ function enhanceBrowserFetchError(
timeoutMs: number,
): Error {
const cause = unwrapCause(err);
const code =
(cause && typeof cause === "object" && "code" in cause
? String((cause as { code?: unknown }).code ?? "")
: "") ||
(err && typeof err === "object" && "code" in err
? String((err as { code?: unknown }).code ?? "")
: "");
const code = extractErrorCode(cause) ?? extractErrorCode(err) ?? "";
const hint =
"Start (or restart) the Clawdis gateway (Clawdis.app menubar, or `clawdis gateway`) and try again.";
@@ -32,7 +28,7 @@ function enhanceBrowserFetchError(
);
}
const msg = String(err);
const msg = formatErrorMessage(err);
if (msg.toLowerCase().includes("abort")) {
return new Error(
`Can't reach the clawd browser control server at ${url} (timed out after ${timeoutMs}ms). ${hint}`,

View File

@@ -128,9 +128,7 @@ describe("pw-ai", () => {
const { chromium } = await import("playwright-core");
const p1 = createPage({ targetId: "T1", snapshotFull: "ONE" });
const browser = createBrowser([p1.page]);
const connect = chromium.connectOverCDP as unknown as ReturnType<
typeof vi.fn
>;
const connect = vi.spyOn(chromium, "connectOverCDP");
connect.mockResolvedValue(browser);
const mod = await importModule();

View File

@@ -1,3 +1,4 @@
import type { BrowserFormField } from "./client-actions-core.js";
import {
type BrowserConsoleMessage,
ensurePageState,
@@ -168,18 +169,29 @@ export async function typeViaPlaywright(opts: {
export async function fillFormViaPlaywright(opts: {
cdpPort: number;
targetId?: string;
fields: Array<Record<string, unknown>>;
fields: BrowserFormField[];
}): Promise<void> {
const page = await getPageForTargetId(opts);
ensurePageState(page);
for (const field of opts.fields) {
const ref = String(field.ref ?? "").trim();
const type = String(field.type ?? "").trim();
const value = String(field.value ?? "");
const ref = field.ref.trim();
const type = field.type.trim();
const rawValue = field.value;
const value =
typeof rawValue === "string"
? rawValue
: typeof rawValue === "number" || typeof rawValue === "boolean"
? String(rawValue)
: "";
if (!ref || !type) continue;
const locator = refLocator(page, ref);
if (type === "checkbox" || type === "radio") {
await locator.setChecked(value === "true");
const checked =
rawValue === true ||
rawValue === 1 ||
rawValue === "1" ||
rawValue === "true";
await locator.setChecked(checked);
continue;
}
await locator.fill(value);
@@ -199,18 +211,47 @@ export async function evaluateViaPlaywright(opts: {
if (opts.ref) {
const locator = refLocator(page, opts.ref);
return await locator.evaluate((el, fnBody) => {
const runner = new Function(
"element",
`"use strict"; const fn = ${fnBody}; return fn(element);`,
) as (element: Element) => unknown;
return runner(el as Element);
const compileRunner = (body: string) => {
const inner = `"use strict"; const candidate = ${body}; return typeof candidate === "function" ? candidate(element) : candidate;`;
// This intentionally evaluates user-supplied code in the browser context.
// oxlint-disable-next-line typescript-eslint/no-implied-eval
return new Function("element", inner) as (element: Element) => unknown;
};
let compiled: unknown;
try {
compiled = compileRunner(fnBody);
} catch (err) {
const message =
err instanceof Error
? err.message
: typeof err === "string"
? err
: "invalid expression";
throw new Error(`Invalid evaluate function: ${message}`);
}
return (compiled as (element: Element) => unknown)(el as Element);
}, fnText);
}
return await page.evaluate((fnBody) => {
const runner = new Function(
`"use strict"; const fn = ${fnBody}; return fn();`,
) as () => unknown;
return runner();
const compileRunner = (body: string) => {
const inner = `"use strict"; const candidate = ${body}; return typeof candidate === "function" ? candidate() : candidate;`;
// This intentionally evaluates user-supplied code in the browser context.
// oxlint-disable-next-line typescript-eslint/no-implied-eval
return new Function(inner) as () => unknown;
};
let compiled: unknown;
try {
compiled = compileRunner(fnBody);
} catch (err) {
const message =
err instanceof Error
? err.message
: typeof err === "string"
? err
: "invalid expression";
throw new Error(`Invalid evaluate function: ${message}`);
}
return (compiled as () => unknown)();
}, fnText);
}

View File

@@ -4,6 +4,7 @@ import type express from "express";
import { ensureMediaDir, saveMediaBuffer } from "../../media/store.js";
import { captureScreenshot, snapshotAria } from "../cdp.js";
import type { BrowserFormField } from "../client-actions-core.js";
import {
DEFAULT_BROWSER_SCREENSHOT_MAX_BYTES,
DEFAULT_BROWSER_SCREENSHOT_MAX_SIDE,
@@ -236,11 +237,24 @@ export function registerBrowserAgentRoutes(
return res.json({ ok: true, targetId: tab.targetId });
}
case "fill": {
const fields = Array.isArray(body.fields)
? (body.fields as Array<Record<string, unknown>>)
: null;
if (!fields?.length)
return jsonError(res, 400, "fields are required");
const rawFields = Array.isArray(body.fields) ? body.fields : [];
const fields = rawFields
.map((field) => {
if (!field || typeof field !== "object") return null;
const rec = field as Record<string, unknown>;
const ref = toStringOrEmpty(rec.ref);
const type = toStringOrEmpty(rec.type);
if (!ref || !type) return null;
const value =
typeof rec.value === "string" ||
typeof rec.value === "number" ||
typeof rec.value === "boolean"
? rec.value
: undefined;
return { ref, type, value };
})
.filter((field): field is BrowserFormField => Boolean(field));
if (!fields.length) return jsonError(res, 400, "fields are required");
await pw.fillFormViaPlaywright({
cdpPort,
targetId: tab.targetId,

View File

@@ -9,7 +9,11 @@ export function jsonError(
}
export function toStringOrEmpty(value: unknown) {
return typeof value === "string" ? value.trim() : String(value ?? "").trim();
if (typeof value === "string") return value.trim();
if (typeof value === "number" || typeof value === "boolean") {
return String(value).trim();
}
return "";
}
export function toNumber(value: unknown) {