1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
|
import { createDaemonTransport } from "../../src/cdp/daemon-transport";
import { createBrowserClient } from "../../src/client";
import { fillTool, selectOptionTool, setCheckedTool } from "../../src/domains/form";
import { fillFormTool } from "../../src/domains/form-batch";
let passed = 0;
let failed = 0;
const check = (cond: boolean, label: string): void => {
if (cond) { passed++; console.log(` ✓ ${label}`); }
else { failed++; console.error(` ✗ ${label}`); }
};
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>`;
const publishRefs = async (client: any, names: ReadonlyArray<string>): Promise<Record<string, string | undefined>> => {
const ax = await client.session().call("Accessibility.getFullAXTree", {});
if (!ax.success) return {};
const byName = new Map<string, number>();
for (const n of (ax.data as any).nodes as any[]) {
if (typeof n?.name?.value === "string" && typeof n.backendDOMNodeId === "number" && !byName.has(n.name.value)) {
byName.set(n.name.value, n.backendDOMNodeId);
}
}
const refMap = new Map<string, number>();
const refSig = new Map<string, string>();
const out: Record<string, string | undefined> = {};
names.forEach((name, i) => {
const backendId = byName.get(name);
if (backendId === undefined) return;
const ref = `e${i + 1}`;
refMap.set(ref, backendId);
refSig.set(ref, `|${name}||`);
out[name] = ref;
});
client.session().setRefMap(refMap, refSig);
return out;
};
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}`);
await new Promise((r) => setTimeout(r, 300));
const byName = await publishRefs(client, ["plain", "ta", "controlled", "country", "agree", "bio"]);
const refs = {
plain: byName["plain"],
ta: byName["ta"],
controlled: byName["controlled"],
sel: byName["country"],
chk: byName["agree"],
editable: byName["bio"],
};
for (const [k, v] of Object.entries(refs)) check(typeof v === "string", `ref present for ${k} (${v})`);
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);
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}`);
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");
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");
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");
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); });
|