diff options
Diffstat (limited to 'src/domains/form.ts')
| -rw-r--r-- | src/domains/form.ts | 117 |
1 files changed, 2 insertions, 115 deletions
diff --git a/src/domains/form.ts b/src/domains/form.ts index 8af43c3..577eb5d 100644 --- a/src/domains/form.ts +++ b/src/domains/form.ts @@ -1,11 +1,9 @@ import { Type } from "typebox"; -import type { BrowserClient } from "../client"; -import { safeJs } from "../util/js-template"; import { type Result, err, ok } from "../util/result"; import { defineBrowserTool, type ToolErr, type ToolOk } from "../util/tool"; -import { cdpCall, evalJs } from "./cdp-call"; +import { detailsOf, isPageResult, type PageResult, resolveAndCall, runOnElement } from "./element-call"; import { fillBody } from "./fill-engine"; -import { interactiveDiff, resolveRefToObjectId } from "./ref-resolve"; +import { interactiveDiff } from "./ref-resolve"; const FillArgs = Type.Object({ ref: Type.Optional( @@ -18,51 +16,6 @@ const FillArgs = Type.Object({ value: Type.String({ description: "Value to set in the field" }), }); -const runOnElement = async ( - client: BrowserClient, - opts: { - ref?: string | undefined; - selector?: string | undefined; - fnBody: string; - arg: unknown; - notFound?: unknown; - }, -): Promise<Result<unknown, ToolErr>> => { - if (opts.ref !== undefined) { - const objectId = await resolveRefToObjectId(client, opts.ref); - if (!objectId.success) return objectId; - const r = await cdpCall(client, "Runtime.callFunctionOn", { - objectId: objectId.data, - functionDeclaration: `function (arg) { ${opts.fnBody} }`, - arguments: [{ value: opts.arg }], - returnByValue: true, - }); - if (!r.success) return r; - if (r.data.exceptionDetails !== undefined) { - return err({ - kind: "cdp_error", - message: `page function threw: ${r.data.exceptionDetails.text}`, - details: { ref: opts.ref }, - }); - } - return ok(r.data.result.value); - } - if (opts.selector === undefined) { - return err({ kind: "invalid_state", message: "Provide either `ref` or `selector`." }); - } - // fnBody is harness-authored, not user input; only the selector and value are interpolated, and those go through safeJs. - const prelude = safeJs` - (() => { - const el = document.querySelector(${opts.selector}); - if (!el) return ${opts.notFound ?? { status: "not_found" }}; - const __arg = ${opts.arg}; - return (function (arg) {`; - const expr = `${prelude} ${opts.fnBody} }).call(el, __arg); })()`; - const r = await evalJs(client, expr); - if (!r.success) return r; - return ok(r.data); -}; - const FILL_BODY = `const value = arg;\n${fillBody({ rejectSelect: true, focusFirst: false })}`; const NOT_FOUND: PageResult = { ok: false, reason: "not_found" }; @@ -281,72 +234,6 @@ export const selectOptionTool = defineBrowserTool({ }, }); -export type PageResult = { - readonly ok: boolean; - readonly reason?: string; - readonly kind?: string; - readonly value?: unknown; - readonly text?: string; - readonly checked?: boolean; - readonly changed?: boolean; - readonly tag?: string; - readonly options?: ReadonlyArray<{ value: string; text: string }>; -}; - -const isOption = (v: unknown): v is { readonly value: string; readonly text: string } => - typeof v === "object" && v !== null - && "value" in v && typeof v.value === "string" - && "text" in v && typeof v.text === "string"; - -const isPageResult = (v: unknown): v is PageResult => { - if (typeof v !== "object" || v === null) return false; - if (!("ok" in v) || typeof v.ok !== "boolean") return false; - if ("reason" in v && typeof v.reason !== "string") return false; - if ("kind" in v && typeof v.kind !== "string") return false; - if ("text" in v && typeof v.text !== "string") return false; - if ("checked" in v && typeof v.checked !== "boolean") return false; - if ("changed" in v && typeof v.changed !== "boolean") return false; - if ("tag" in v && typeof v.tag !== "string") return false; - if ("options" in v && !(Array.isArray(v.options) && v.options.every(isOption))) return false; - return true; -}; - -export const resolveAndCall = async ( - client: BrowserClient, - ref: string, - functionDeclaration: string, - args: ReadonlyArray<unknown>, -): Promise<Result<PageResult, ToolErr>> => { - const objectId = await resolveRefToObjectId(client, ref); - if (!objectId.success) return objectId; - const handle = objectId.data; - try { - const called = await cdpCall(client, "Runtime.callFunctionOn", { - objectId: handle, - functionDeclaration, - arguments: args.map((v) => ({ value: v })), - returnByValue: true, - awaitPromise: true, - }); - if (!called.success) return err({ ...called.error, details: { ref } }); - if (called.data.exceptionDetails !== undefined) { - return err({ kind: "cdp_error", message: `page function threw: ${JSON.stringify(called.data.exceptionDetails)}`, details: { ref } }); - } - const value = called.data.result.value; - if (!isPageResult(value)) { - return err({ kind: "internal", message: "page function returned no result object", details: { ref } }); - } - return ok(value); - } finally { - await cdpCall(client, "Runtime.releaseObject", { objectId: handle }); - } -}; - -export const detailsOf = (ref: string, res: PageResult, extra?: Readonly<Record<string, unknown>>): Readonly<Record<string, unknown>> => { - const { ok: _ok, ...rest } = res; - return { ref, ...rest, ...(extra ?? {}) }; -}; - const CHECK_FN = ` function(checked) { const el = this; |
