aboutsummaryrefslogtreecommitdiff
path: root/src/domains/form.ts
diff options
context:
space:
mode:
authoramankumarsingh77 <amankumarsingh7702@gmail.com>2026-07-27 23:26:50 +0530
committerAman Kumar <145339995+amankumarsingh77@users.noreply.github.com>2026-08-02 19:12:46 +0530
commit9f2f75dd9281153888776e5d39d4926655c15f2a (patch)
tree89c23c8ad333b5096d39b661b142eaad544cf004 /src/domains/form.ts
parent931576443dcb990761cb7e68e354465145f9292d (diff)
refactor(domains): drop CDP casts, share the box-model unwrap
ref-resolve.ts and snapshot.ts had the same box-model unwrap, both using data.model! to work around an untyped response. Extracted to domains/box.ts; the typed schema makes the assertions unnecessary. Both derived the centre identically (quad corner + half the model width/height), so centreOf keeps that derivation; the shared zero-size rejection moved into boxOf, and snapshot keeps its own off-viewport (x/y < 0) filter. evalInIsolatedTab now goes through evaluateJson with the tab's own sessionId and a caller-supplied guard; both callers evaluate expressions ending in JSON.stringify, so they pass isJsonText rather than a check that checks nothing. Runtime.evaluate/callFunctionOn result casts in form.ts, forms.ts and keyboard.ts are replaced by named guards over the in-page result shapes. Runtime.callFunctionOn's params schema gained arguments/returnByValue/ awaitPromise/userGesture: four domain call sites legitimately pass them and typebox's additionalProperties:true does not widen the static type, so the params type was rejecting valid CDP fields. Params schemas are type-only (only results are compiled), so this is a compile-time widening with no runtime effect. tsc is green again for both tsconfigs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'src/domains/form.ts')
-rw-r--r--src/domains/form.ts45
1 files changed, 41 insertions, 4 deletions
diff --git a/src/domains/form.ts b/src/domains/form.ts
index 063cfaf..95516c5 100644
--- a/src/domains/form.ts
+++ b/src/domains/form.ts
@@ -40,7 +40,7 @@ const runOnElement = async (
returnByValue: true,
});
if (!r.success) return err({ kind: "cdp_error", message: r.error.message });
- return ok((r.data as { result?: { value?: unknown } }).result?.value);
+ return ok(r.data.result.value);
}
if (opts.selector === undefined) {
return err({ kind: "invalid_state", message: "Provide either `ref` or `selector`." });
@@ -69,6 +69,18 @@ type FillResult =
| { status: "not_found" }
| { status: "not_fillable"; tag: string };
+const isFillResult = (v: unknown): v is FillResult => {
+ if (typeof v !== "object" || v === null || !("status" in v)) return false;
+ if (v.status === "not_found") return true;
+ if (v.status === "not_fillable") return "tag" in v && typeof v.tag === "string";
+ if (v.status === "ok") {
+ return "tag" in v && typeof v.tag === "string"
+ && "kind" in v && typeof v.kind === "string"
+ && "value" in v && typeof v.value === "string";
+ }
+ return false;
+};
+
// Element-scoped fill logic. `this` is the target element, `arg` is the value.
// Write via the native prototype value setter so React's _valueTracker
// registers the change — assigning el.value directly does NOT, which is why
@@ -127,7 +139,7 @@ export const fillTool = defineBrowserTool({
async handler(args, { client }): Promise<Result<ToolOk, ToolErr>> {
const r = await runOnElement(client, { ref: args.ref, selector: args.selector, fnBody: FILL_FN, arg: args.value });
if (!r.success) return r;
- const res = r.data as FillResult | undefined;
+ const res = isFillResult(r.data) ? r.data : undefined;
const target = args.ref ?? args.selector ?? "";
if (res === undefined || res.status === "not_found") {
return err({
@@ -160,6 +172,13 @@ const FocusArgs = Type.Object({
type FocusResult = { status: "ok"; tag: string } | { status: "not_found" } | { status: "not_focusable"; tag: string };
+const isFocusResult = (v: unknown): v is FocusResult => {
+ if (typeof v !== "object" || v === null || !("status" in v)) return false;
+ if (v.status === "not_found") return true;
+ if (v.status === "ok" || v.status === "not_focusable") return "tag" in v && typeof v.tag === "string";
+ return false;
+};
+
const FOCUS_FN = `
const el = this;
if (typeof el.focus !== "function") return { status: "not_focusable", tag: el.tagName };
@@ -182,7 +201,7 @@ export const focusTool = defineBrowserTool({
async handler(args, { client }): Promise<Result<ToolOk, ToolErr>> {
const r = await runOnElement(client, { ref: args.ref, selector: args.selector, fnBody: FOCUS_FN, arg: "" });
if (!r.success) return r;
- const res = r.data as FocusResult | undefined;
+ const res = isFocusResult(r.data) ? r.data : undefined;
const target = args.ref ?? args.selector ?? "";
if (res === undefined || res.status === "not_found") {
return err({ kind: "invalid_state", message: `No element matched: ${target}`, details: { ref: args.ref, selector: args.selector } });
@@ -210,6 +229,24 @@ type SelectResult =
| { status: "not_select"; tag: string }
| { status: "no_match"; options: ReadonlyArray<{ value: string; label: string }> };
+const isSelectOption = (v: unknown): v is { value: string; label: string } =>
+ typeof v === "object" && v !== null
+ && "value" in v && typeof v.value === "string"
+ && "label" in v && typeof v.label === "string";
+
+const isSelectResult = (v: unknown): v is SelectResult => {
+ if (typeof v !== "object" || v === null || !("status" in v)) return false;
+ if (v.status === "not_found") return true;
+ if (v.status === "not_select") return "tag" in v && typeof v.tag === "string";
+ if (v.status === "ok") {
+ return "value" in v && typeof v.value === "string" && "label" in v && typeof v.label === "string";
+ }
+ if (v.status === "no_match") {
+ return "options" in v && Array.isArray(v.options) && v.options.every(isSelectOption);
+ }
+ return false;
+};
+
// `this` is the <select>; `arg` is { wv, wl, wi } (null = not provided).
const SELECT_FN = `
const el = this;
@@ -248,7 +285,7 @@ export const selectOptionTool = defineBrowserTool({
const arg = { wv: args.value ?? null, wl: args.label ?? null, wi: args.index ?? null };
const r = await runOnElement(client, { ref: args.ref, selector: args.selector, fnBody: SELECT_FN, arg });
if (!r.success) return r;
- const res = r.data as SelectResult | undefined;
+ const res = isSelectResult(r.data) ? r.data : undefined;
const target = args.ref ?? args.selector ?? "";
if (res === undefined || res.status === "not_found") {
return err({ kind: "invalid_state", message: `No element matched: ${target}`, details: { ref: args.ref, selector: args.selector } });