From e46f080c03a511b626b21455cfd6094554b02733 Mon Sep 17 00:00:00 2001 From: amankumarsingh77 Date: Tue, 28 Jul 2026 00:28:45 +0530 Subject: fix(domains): restore fill_form and set_checked, drop the stale fork MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit domains/forms.ts was a stale fork of form.ts imported only by a manual test. It held two working tools the registry never registered: fill_form and set_checked. Ported both onto the current pattern and deleted the fork; form.ts versions of the shared tools win. The fork's tools took ref as Type.Integer (a raw CDP backendNodeId). browser_snapshot has since moved to string refs ('e7') resolved through the session ref map, so as written the tools were not callable at all. Ported them onto resolveRefToObjectId and Type.String refs; the only user-facing strings that changed are the four that documented the old [ref=N] syntax. All page-side JS is carried byte-for-byte. form.ts exceeded 250 lines, so fillForm moved to form-batch.ts per the single-field/batch split. form.ts is still 425 lines — the pre-existing three tools account for that, so the split does not reach the target. test/manual/forms-test.ts is repointed and adapted to publish an 'eN' ref map, but NOT executed — it needs a live Chrome. Type-checked only. Co-Authored-By: Claude Opus 5 (1M context) --- src/domains/form-batch.ts | 113 ++++++++++++++ src/domains/form.ts | 112 ++++++++++++++ src/domains/forms.ts | 375 ---------------------------------------------- src/registry.ts | 5 +- 4 files changed, 229 insertions(+), 376 deletions(-) create mode 100644 src/domains/form-batch.ts delete mode 100644 src/domains/forms.ts (limited to 'src') diff --git a/src/domains/form-batch.ts b/src/domains/form-batch.ts new file mode 100644 index 0000000..b926025 --- /dev/null +++ b/src/domains/form-batch.ts @@ -0,0 +1,113 @@ +import { Type } from "typebox"; +import { type Result, ok } from "../util/result"; +import { defineBrowserTool, type ToolErr, type ToolOk } from "../util/tool"; +import { detailsOf, resolveAndCall } from "./form"; + +const FILL_FN = ` +function(value) { + const el = this; + if (!el || el.nodeType !== 1) return { ok: false, reason: "ref does not point to an element" }; + if (el.disabled) return { ok: false, reason: "element is disabled" }; + const tag = (el.tagName || "").toLowerCase(); + const type = (el.type || "").toLowerCase(); + try { el.focus(); } catch (e) {} + const fire = function(t) { el.dispatchEvent(new Event(t, { bubbles: true })); }; + + if (tag === "select") { + const want = String(value); + let matched = null; + for (let i = 0; i < el.options.length; i++) { + const o = el.options[i]; + if (o.value === want || o.label === want || o.text === want) { matched = o; break; } + } + if (!matched) { + const opts = []; + for (let i = 0; i < el.options.length; i++) opts.push({ value: el.options[i].value, text: el.options[i].text }); + return { ok: false, reason: "no matching option", kind: "select", options: opts }; + } + el.value = matched.value; + fire("input"); fire("change"); + return { ok: true, kind: "select", value: el.value, text: matched.text }; + } + + if (tag === "input" && (type === "checkbox" || type === "radio")) { + const want = value === true || value === "true" || value === "on" || value === 1; + if (el.checked !== want) { + try { el.click(); } catch (e) {} + if (el.checked !== want) { el.checked = want; fire("input"); fire("change"); } + } + return { ok: true, kind: type, checked: el.checked }; + } + + if (el.isContentEditable) { + el.textContent = String(value); + fire("input"); fire("change"); + return { ok: true, kind: "contenteditable", value: el.textContent }; + } + + if (tag === "input" || tag === "textarea") { + const proto = tag === "textarea" ? window.HTMLTextAreaElement.prototype : window.HTMLInputElement.prototype; + const desc = Object.getOwnPropertyDescriptor(proto, "value"); + if (desc && desc.set) desc.set.call(el, String(value)); + else el.value = String(value); + fire("input"); fire("change"); + return { ok: true, kind: tag, value: el.value }; + } + + return { ok: false, reason: "element is not a fillable field (tag=" + tag + ")" }; +}`; + +const FieldValue = Type.Union([Type.String(), Type.Boolean()], { + description: "Value to set. String for text inputs / textareas / selects / contenteditable; boolean for checkboxes & radios.", +}); + +const FillFormArgs = Type.Object({ + fields: Type.Array( + Type.Object({ + ref: Type.String({ description: "Stable element ref from browser_snapshot (e.g. 'e7')." }), + value: FieldValue, + }), + { minItems: 1, description: "Fields to fill in one batch, each by ref." }, + ), +}); + +export const fillFormTool = defineBrowserTool({ + name: "browser_fill_form", + label: "Browser Fill Form", + description: + "Fill multiple form fields in one call — the efficient way to complete a form. Each field is { ref, value }; refs come from browser_snapshot's [eN]. Handles text inputs, textareas, selects, checkboxes, radios, and contenteditable, drives React/Vue controlled inputs correctly, and reports the resulting value of every field.", + promptSnippet: "Fill many form fields at once by ref — React-safe, self-confirming", + promptGuidelines: [ + "Preferred for forms: snapshot once, then fill all fields in a single browser_fill_form call.", + "Each field: { ref, value }. value is a string for text/select/contenteditable, boolean for checkbox/radio.", + "The result summarizes how many fields filled cleanly and flags any value mismatches or per-field errors.", + "Re-run browser_snapshot afterwards to verify the form state if needed.", + ], + parameters: FillFormArgs, + concurrency: "serialized", + async handler(args, { client }): Promise> { + const results: Array>> = []; + let okCount = 0; + const issues: string[] = []; + for (const f of args.fields) { + const r = await resolveAndCall(client, f.ref, FILL_FN, [f.value]); + if (!r.success) { + results.push({ ref: f.ref, ok: false, error: r.error.message }); + issues.push(`ref ${f.ref} (${r.error.message})`); + continue; + } + const res = r.data; + const wanted = typeof f.value === "boolean" ? undefined : String(f.value); + const got = res.value !== undefined ? String(res.value) : undefined; + const mismatch = wanted !== undefined && got !== undefined && got !== wanted; + const fieldOk = res.ok === true && !mismatch; + if (fieldOk) okCount++; + else issues.push(`ref ${f.ref} (${res.ok === false ? (res.reason ?? "failed") : "value mismatch"})`); + results.push({ ref: f.ref, ok: fieldOk, mismatch, ...detailsOf(f.ref, res) }); + } + const text = + `Filled ${okCount}/${args.fields.length} fields` + + (issues.length > 0 ? `; issues: ${issues.join(", ")}` : ""); + return ok({ text, details: { results } }); + }, +}); diff --git a/src/domains/form.ts b/src/domains/form.ts index a1ae7f5..26268d9 100644 --- a/src/domains/form.ts +++ b/src/domains/form.ts @@ -311,3 +311,115 @@ 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 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 ("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, +): Promise> => { + const objectId = await resolveRefToObjectId(client, ref); + if (!objectId.success) return objectId; + const handle = objectId.data; + try { + const called = await client.session().call("Runtime.callFunctionOn", { + objectId: handle, + functionDeclaration, + arguments: args.map((v) => ({ value: v })), + returnByValue: true, + awaitPromise: true, + }); + if (!called.success) return err({ kind: "cdp_error", message: called.error.message, 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 client.session().call("Runtime.releaseObject", { objectId: handle }); + } +}; + +export const detailsOf = (ref: string, res: PageResult, extra?: Readonly>): Readonly> => { + const { ok: _ok, ...rest } = res; + return { ref, ...rest, ...(extra ?? {}) }; +}; + +const CHECK_FN = ` +function(checked) { + const el = this; + const tag = (el.tagName || "").toLowerCase(); + const type = (el.type || "").toLowerCase(); + if (tag !== "input" || (type !== "checkbox" && type !== "radio")) return { ok: false, reason: "ref is not a checkbox or radio" }; + if (el.disabled) return { ok: false, reason: "element is disabled" }; + const want = checked === true; + let changed = false; + if (el.checked !== want) { + try { el.click(); } catch (e) {} + if (el.checked !== want) { el.checked = want; el.dispatchEvent(new Event("change", { bubbles: true })); } + changed = true; + } + return { ok: true, checked: el.checked, changed: changed }; +}`; + +const SetCheckedArgs = Type.Object({ + ref: Type.String({ description: "Stable element ref of a checkbox or radio from browser_snapshot (e.g. 'e7')." }), + checked: Type.Boolean({ description: "Desired checked state." }), +}); + +export const setCheckedTool = defineBrowserTool({ + name: "browser_set_checked", + label: "Browser Set Checked", + description: + "Set a checkbox or radio to a desired checked state by ref. Only acts if the current state differs, fires change, and reports the final state. Get the ref from browser_snapshot's [eN].", + promptSnippet: "Set a checkbox/radio checked state by ref", + promptGuidelines: [ + "checked:true ticks the box, checked:false unticks it. Idempotent — no-op if already in the desired state.", + "For radios, set checked:true on the option you want; the group deselects the others.", + ], + parameters: SetCheckedArgs, + concurrency: "serialized", + async handler(args, { client }): Promise> { + const r = await resolveAndCall(client, args.ref, CHECK_FN, [args.checked]); + if (!r.success) return r; + const res = r.data; + if (!res.ok) { + return err({ kind: "invalid_state", message: `Could not set checked on ref ${args.ref}: ${res.reason ?? "unknown reason"}`, details: detailsOf(args.ref, res) }); + } + return ok({ + text: `Set ref ${args.ref} checked=${res.checked}` + (res.changed === false ? " (already)" : ""), + details: detailsOf(args.ref, res), + }); + }, +}); diff --git a/src/domains/forms.ts b/src/domains/forms.ts deleted file mode 100644 index ed64a74..0000000 --- a/src/domains/forms.ts +++ /dev/null @@ -1,375 +0,0 @@ -import { Type } from "typebox"; -import type { BrowserClient } from "../client"; -import { type Result, err, ok } from "../util/result"; -import { defineBrowserTool, type ToolErr, type ToolOk } from "../util/tool"; - -// ── Page-side functions ────────────────────────────────────────────────────── -// These run inside the page via Runtime.callFunctionOn with `this` bound to the -// resolved element. They are NOT string-interpolated with user data — the value -// is passed as a structured `arguments` entry, so safeJs is unnecessary here. - -/** - * Universal field setter. Auto-detects the element type and applies the right - * strategy, then reads the resulting value back so the caller can confirm. - * - * The /