diff options
| author | Aman Kumar <145339995+amankumarsingh77@users.noreply.github.com> | 2026-07-22 00:59:58 +0530 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-07-22 00:59:58 +0530 |
| commit | fe00d3361dece3d35f788d3eeb1c750eedbf6294 (patch) | |
| tree | a51b8ffe3cddb1aa8d457784f71cc1b2b0b61d56 /test/manual/forms-test.ts | |
| parent | 7de0beaf2cf713afc50c573e941fa279ec6df9d9 (diff) | |
feat: add forms domain and durable window binding (v0.9.0) (#14)
- Add forms domain with a universal field setter that auto-detects element
type and fires framework-compatible input events (React native-setter trick)
- Add window-binding helper to capture and bind the real Chrome windowId as the
session's durable window identity on every new-window code path
- Wire supporting changes across CDP session/ownership, client, navigate,
isolated-tab, prompt, and state
- Add manual tests for forms and window binding
- Bump version 0.8.3 -> 0.9.0
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'test/manual/forms-test.ts')
| -rw-r--r-- | test/manual/forms-test.ts | 149 |
1 files changed, 149 insertions, 0 deletions
diff --git a/test/manual/forms-test.ts b/test/manual/forms-test.ts new file mode 100644 index 0000000..f1c79f6 --- /dev/null +++ b/test/manual/forms-test.ts @@ -0,0 +1,149 @@ +/** + * Manual integration test for the form-filling tools. + * + * Exercises browser_snapshot (ref surfacing) + browser_fill_form / browser_fill / + * browser_select_option / browser_set_checked against a data:-URL fixture that + * includes a plain input, a textarea, a "controlled" input (commits its model + * only on the `input` event — the realistic failure mode for naive value-setting), + * a native <select>, a checkbox, and a contenteditable. + * + * Requires the daemon running + a real Chrome (same as live-daemon-test.ts). + * Run: npx tsx test/manual/forms-test.ts + */ +import { createDaemonTransport } from "../../src/cdp/daemon-transport"; +import { createBrowserClient } from "../../src/client"; +import { fillTool, fillFormTool, selectOptionTool, setCheckedTool } from "../../src/domains/forms"; + +// NOTE: we intentionally do NOT import snapshotTool here — it pulls in pi-tui / +// pi-coding-agent value imports that don't resolve under bare tsx. Instead we +// derive refs from Accessibility.getFullAXTree's backendDOMNodeId, which is the +// exact value browser_snapshot surfaces as [ref=N]. The snapshot's [ref=N] +// rendering is covered by `npm run typecheck` + the real-page smoke test. + +let passed = 0; +let failed = 0; +const check = (cond: boolean, label: string): void => { + if (cond) { passed++; console.log(` ✓ ${label}`); } + else { failed++; console.error(` ✗ ${label}`); } +}; + +// Minimal handler context — the form/snapshot handlers only read `client`. +const mkCtx = (client: any): any => ({ + client, + signal: undefined, + onUpdate: () => {}, + extensionCtx: { cwd: process.cwd() }, +}); + +const FIXTURE = ` +<!doctype html><html><body> + <h1>Form fixture</h1> + <input id="plain" aria-label="plain" /> + <textarea id="ta" aria-label="ta"></textarea> + <input id="controlled" aria-label="controlled" /> + <span id="model"></span> + <select id="sel" aria-label="country"> + <option value="us">United States</option> + <option value="uk">United Kingdom</option> + <option value="ca">Canada</option> + </select> + <input type="checkbox" id="chk" aria-label="agree" /> + <div id="editable" role="textbox" aria-label="bio" contenteditable="true"></div> + <script> + // Controlled input: its "model" updates ONLY when an input event fires. + // A naive value-set that skips the input event leaves the model stale. + var model = ""; + var c = document.getElementById("controlled"); + c.addEventListener("input", function () { model = c.value; document.getElementById("model").textContent = model; }); + window.__model = function () { return model; }; + </script> +</body></html>`; + +// Resolve a ref by accessible name via the AX tree — the same backendDOMNodeId +// that browser_snapshot prints as [ref=N]. +const refByName = async (client: any, name: string): Promise<number | undefined> => { + const ax = await client.session().call("Accessibility.getFullAXTree", {}); + if (!ax.success) return undefined; + for (const n of (ax.data as any).nodes as any[]) { + if (n?.name?.value === name && typeof n.backendDOMNodeId === "number") return n.backendDOMNodeId; + } + return undefined; +}; + +async function main(): Promise<void> { + const transport = createDaemonTransport("pi-forms-test"); + const client = createBrowserClient({ namespace: "pi-forms-test", transport }); + const ctx = mkCtx(client); + + const started = await client.start(); + if (!started.success) { console.error("Could not start client:", started.error.message); process.exit(1); } + + const url = "data:text/html," + encodeURIComponent(FIXTURE); + const tab = await client.newTab(url); + check(tab.success, `Opened fixture tab: ${tab.success ? "ok" : tab.error.message}`); + // Give the inline script a beat to wire up listeners. + await new Promise((r) => setTimeout(r, 300)); + + // ── Resolve refs (the backendDOMNodeId browser_snapshot prints as [ref=N]) ── + const refs = { + plain: await refByName(client, "plain"), + ta: await refByName(client, "ta"), + controlled: await refByName(client, "controlled"), + sel: await refByName(client, "country"), + chk: await refByName(client, "agree"), + editable: await refByName(client, "bio"), + }; + for (const [k, v] of Object.entries(refs)) check(typeof v === "number", `ref present for ${k} (${v})`); + + // ── Batch fill text fields ────────────────────────────────────────────────── + const fill = await fillFormTool.handler({ + fields: [ + { ref: refs.plain!, value: "hello plain" }, + { ref: refs.ta!, value: "multi\nline" }, + { ref: refs.controlled!, value: "ctrl-value" }, + { ref: refs.editable!, value: "my bio" }, + ], + }, ctx); + check(fill.success, "browser_fill_form succeeded"); + if (fill.success) console.log(" " + fill.data.text); + + // ── Select + checkbox ─────────────────────────────────────────────────────── + const sel = await selectOptionTool.handler({ ref: refs.sel!, label: "Canada" }, ctx); + check(sel.success, `browser_select_option by label: ${sel.success ? sel.data.text : (sel as any).error.message}`); + const chk = await setCheckedTool.handler({ ref: refs.chk!, checked: true }, ctx); + check(chk.success, `browser_set_checked: ${chk.success ? chk.data.text : (chk as any).error.message}`); + + // ── Verify committed state via JS reads ───────────────────────────────────── + const read = async (expr: string): Promise<unknown> => { + const r = await client.evaluateJs(expr); + return r.success ? r.data : `ERR:${r.error.message}`; + }; + check((await read("document.getElementById('plain').value")) === "hello plain", "plain input value committed"); + check((await read("document.getElementById('ta').value")) === "multi\nline", "textarea value committed"); + // The controlled input's MODEL (input-event-driven) must reflect the fill — + // proves browser_fill fired the input event, not just set the DOM value. + check((await read("window.__model()")) === "ctrl-value", "controlled input model committed (input event fired)"); + check((await read("document.getElementById('editable').textContent")) === "my bio", "contenteditable text committed"); + check((await read("document.getElementById('sel').value")) === "ca", "select value committed"); + check((await read("document.getElementById('chk').checked")) === true, "checkbox checked committed"); + + // ── No-matching-option path ───────────────────────────────────────────────── + const badSel = await selectOptionTool.handler({ ref: refs.sel!, label: "Atlantis" }, ctx); + check(!badSel.success && (badSel as any).error.kind === "invalid_state", "select with no match → invalid_state error"); + + // ── Stale ref path ────────────────────────────────────────────────────────── + const staleRef = refs.plain!; + const nav = await client.session().call("Page.navigate", { url: "data:text/html," + encodeURIComponent("<body>gone</body>") }); + check(nav.success, "navigated away to invalidate refs"); + await new Promise((r) => setTimeout(r, 300)); + const stale = await fillTool.handler({ ref: staleRef, value: "x" }, ctx); + check(!stale.success && (stale as any).error.kind === "invalid_state", "fill on stale ref → invalid_state error"); + + await client.closeTab(tab.success ? (tab.data as string) : ""); + await client.stop(); + + console.log(`\n${passed} passed, ${failed} failed`); + process.exit(failed === 0 ? 0 : 1); +} + +main().catch((e) => { console.error(e); process.exit(1); }); |
