refactor: lint cleanups and helpers
This commit is contained in:
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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}`,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user