diff options
| author | amankumarsingh77 <amankumarsingh7702@gmail.com> | 2026-07-28 19:57:27 +0530 |
|---|---|---|
| committer | Aman Kumar <145339995+amankumarsingh77@users.noreply.github.com> | 2026-08-02 19:12:46 +0530 |
| commit | 8ccff6c98b04666401b041002ca4aab997e1ff22 (patch) | |
| tree | 395ddd4106bd3ce075de50e6413a40ce8a450168 /test | |
| parent | cfa1892e17e3696da004c71a4942d5b12186b12c (diff) | |
fix(domains): report a page-side throw instead of blaming the ref
runOnElement checked only whether Runtime.callFunctionOn reached Chrome, not
whether the function it ran threw. A page-side exception left result.value
undefined, the result guard rejected it, and browser_fill, browser_focus and
browser_select_option all reported "No element matched: <ref>" for an element
that was there — sending the agent to re-snapshot a ref that was fine and
discarding the real error. The throw is now surfaced as cdp_error carrying the
exception text, which is distinguishable from both the no-match and stale-ref
paths; those still fire for a genuinely missing or stale element. The selector
path already reported throws correctly via evaluateJs.
Report: docs/superpowers/reports/2026-07-26-bug-audit.md#f2
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'test')
| -rw-r--r-- | test/domains/form-errors-test.ts | 97 |
1 files changed, 97 insertions, 0 deletions
diff --git a/test/domains/form-errors-test.ts b/test/domains/form-errors-test.ts new file mode 100644 index 0000000..69db740 --- /dev/null +++ b/test/domains/form-errors-test.ts @@ -0,0 +1,97 @@ +import { test, describe } from "node:test"; +import assert from "node:assert/strict"; +import { createFakeClient, type FakeClient } from "./fake-client"; +import { fillTool, focusTool, selectOptionTool } from "../../src/domains/form"; +import type { HandlerContext } from "../../src/util/tool"; +import { ok } from "../../src/util/result"; + +const RESOLVED_NODE = ok({ object: { type: "object", objectId: "obj-1" } }); + +const THREW = ok({ + result: { type: "undefined" }, + exceptionDetails: { text: "Uncaught TypeError: el.dispatchEvent is not a function" }, +}); + +const returned = (value: unknown) => ok({ result: { type: "object", value } }); + +const ctxFor = (fake: FakeClient): HandlerContext => ({ + client: fake.client, + signal: undefined, + onUpdate: () => {}, + extensionCtx: undefined as never, +}); + +const withRef = async (callResult: ReturnType<typeof ok>): Promise<FakeClient> => + createFakeClient({ + refs: [{ ref: "e1", backendId: 101 }], + canned: { "DOM.resolveNode": RESOLVED_NODE, "Runtime.callFunctionOn": callResult }, + }); + +describe("a page-side throw is reported as a throw, not as a missing element", () => { + test("browser_fill surfaces the page exception", async () => { + const fake = await withRef(THREW); + const r = await fillTool.handler({ ref: "e1", value: "x" }, ctxFor(fake)); + assert.equal(r.success, false); + if (r.success) return; + assert.equal(r.error.kind, "cdp_error"); + assert.doesNotMatch(r.error.message, /No element matched/); + assert.match(r.error.message, /dispatchEvent is not a function/); + }); + + test("browser_focus surfaces the page exception", async () => { + const fake = await withRef(THREW); + const r = await focusTool.handler({ ref: "e1" }, ctxFor(fake)); + assert.equal(r.success, false); + if (r.success) return; + assert.equal(r.error.kind, "cdp_error"); + assert.doesNotMatch(r.error.message, /No element matched/); + }); + + test("browser_select_option surfaces the page exception", async () => { + const fake = await withRef(THREW); + const r = await selectOptionTool.handler({ ref: "e1", label: "India" }, ctxFor(fake)); + assert.equal(r.success, false); + if (r.success) return; + assert.equal(r.error.kind, "cdp_error"); + assert.doesNotMatch(r.error.message, /No element matched/); + }); +}); + +describe("a genuinely missing element still reports no match", () => { + test("the selector path reports no match when querySelector finds nothing", async () => { + const fake = await createFakeClient({ + evaluate: () => ok({ ok: false, reason: "not_found" }), + }); + const r = await fillTool.handler({ selector: "#nope", value: "x" }, ctxFor(fake)); + assert.equal(r.success, false); + if (r.success) return; + assert.equal(r.error.kind, "invalid_state"); + assert.match(r.error.message, /No element matched: #nope/); + }); + + test("a stale ref still reports a stale ref", async () => { + const fake = await createFakeClient({ refs: [{ ref: "e1", backendId: 101 }] }); + const r = await fillTool.handler({ ref: "e9", value: "x" }, ctxFor(fake)); + assert.equal(r.success, false); + if (r.success) return; + assert.match(r.error.message, /unknown or stale/); + }); +}); + +describe("a well-formed page result still succeeds", () => { + test("browser_fill reports the value the page read back", async () => { + const fake = await withRef(returned({ ok: true, kind: "text", value: "typed", tag: "INPUT" })); + const r = await fillTool.handler({ ref: "e1", value: "typed" }, ctxFor(fake)); + assert.equal(r.success, true); + if (!r.success) return; + assert.match(r.data.text, /Filled e1 = "typed"/); + }); + + test("browser_fill refuses a disabled field with the page's reason", async () => { + const fake = await withRef(returned({ ok: false, reason: "element is disabled", tag: "INPUT" })); + const r = await fillTool.handler({ ref: "e1", value: "typed" }, ctxFor(fake)); + assert.equal(r.success, false); + if (r.success) return; + assert.match(r.error.message, /element is disabled/); + }); +}); |
