diff options
| author | amankumarsingh77 <amankumarsingh7702@gmail.com> | 2026-07-27 23:26:50 +0530 |
|---|---|---|
| committer | Aman Kumar <145339995+amankumarsingh77@users.noreply.github.com> | 2026-08-02 19:12:46 +0530 |
| commit | 9f2f75dd9281153888776e5d39d4926655c15f2a (patch) | |
| tree | 89c23c8ad333b5096d39b661b142eaad544cf004 /src/domains/form.ts | |
| parent | 931576443dcb990761cb7e68e354465145f9292d (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.ts | 45 |
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 } }); |
