@neuralnomads/codenomad-dev 0.20.1-dev-20261019-7eb401eb → 0.20.1-dev-20261019-e32d9331

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/dist/automation/browser-page.js +97 -0
  2. package/dist/automation/browser-page.js.map +1 -0
  3. package/dist/automation/browser-page.test.js +119 -0
  4. package/dist/automation/browser-page.test.js.map +1 -0
  5. package/dist/automation/page/actions.js +107 -83
  6. package/dist/automation/page/actions.js.map +1 -1
  7. package/dist/automation/page/input.js +130 -0
  8. package/dist/automation/page/input.js.map +1 -0
  9. package/dist/automation/page/input.test.js +71 -0
  10. package/dist/automation/page/input.test.js.map +1 -0
  11. package/dist/automation/page/keys.js +99 -0
  12. package/dist/automation/page/keys.js.map +1 -0
  13. package/dist/automation/page/keys.test.js +32 -0
  14. package/dist/automation/page/keys.test.js.map +1 -0
  15. package/dist/automation/page/parse.js +79 -0
  16. package/dist/automation/page/parse.js.map +1 -0
  17. package/dist/automation/page/protocol.js +143 -0
  18. package/dist/automation/page/protocol.js.map +1 -0
  19. package/dist/developer-cdp.js +7 -10
  20. package/dist/developer-cdp.js.map +1 -1
  21. package/dist/developer-cdp.test.js +49 -15
  22. package/dist/developer-cdp.test.js.map +1 -1
  23. package/dist/opencode/automation/desktop-plugin.js +10 -6
  24. package/dist/opencode/automation/desktop-plugin.js.map +1 -1
  25. package/dist/opencode/automation-plugin.js +33 -41
  26. package/dist/opencode/automation-plugin.js.map +1 -1
  27. package/dist/opencode/automation-plugin.test.js +42 -1
  28. package/dist/opencode/automation-plugin.test.js.map +1 -1
  29. package/dist/plugins/automation/plugin.mjs +230 -40
  30. package/dist/plugins/missions/plugin.mjs +21 -0
  31. package/dist/server/routes/automation-plugin.js +7 -5
  32. package/dist/server/routes/automation-plugin.js.map +1 -1
  33. package/dist/server/routes/automation-plugin.test.js +17 -4
  34. package/dist/server/routes/automation-plugin.test.js.map +1 -1
  35. package/package.json +1 -1
  36. package/public/assets/{App-7u0r589J.js → App-DZMdkLQJ.js} +8 -8
  37. package/public/assets/{App-7u0r589J.js.map → App-DZMdkLQJ.js.map} +1 -1
  38. package/public/assets/{FilesPanel-DmdwCBoM.js → FilesPanel-CgFgL4q7.js} +2 -2
  39. package/public/assets/{FilesPanel-DmdwCBoM.js.map → FilesPanel-CgFgL4q7.js.map} +1 -1
  40. package/public/assets/{StatusTab-CPfdwHzQ.js → StatusTab-CaQz8RwN.js} +2 -2
  41. package/public/assets/{StatusTab-CPfdwHzQ.js.map → StatusTab-CaQz8RwN.js.map} +1 -1
  42. package/public/assets/{client-state-Dbg3RTSW.js → client-state-DikXRKzG.js} +2 -2
  43. package/public/assets/{client-state-Dbg3RTSW.js.map → client-state-DikXRKzG.js.map} +1 -1
  44. package/public/assets/{client-state-_VkciMC3.js → client-state-kpF1cbUu.js} +2 -2
  45. package/public/assets/{client-state-_VkciMC3.js.map → client-state-kpF1cbUu.js.map} +1 -1
  46. package/public/assets/client-state-owner-commands-BMJJQYT-.js +2 -0
  47. package/public/assets/{client-state-owner-commands-C_V-9FL7.js.map → client-state-owner-commands-BMJJQYT-.js.map} +1 -1
  48. package/public/assets/{diff-viewer-EKYtk2Kn.js → diff-viewer-iFIylM56.js} +2 -2
  49. package/public/assets/{diff-viewer-EKYtk2Kn.js.map → diff-viewer-iFIylM56.js.map} +1 -1
  50. package/public/assets/{main-CSjPT-d2.js → main-B0L3f7sD.js} +52 -52
  51. package/public/assets/main-B0L3f7sD.js.map +1 -0
  52. package/public/assets/{markdown-BCaUmWNf.js → markdown-Bh1QlJgA.js} +2 -2
  53. package/public/assets/{markdown-BCaUmWNf.js.map → markdown-Bh1QlJgA.js.map} +1 -1
  54. package/public/assets/{opener-startup-state-settings-card-DcDRs1t6.js → opener-startup-state-settings-card-mzh22bq8.js} +2 -2
  55. package/public/assets/{opener-startup-state-settings-card-DcDRs1t6.js.map → opener-startup-state-settings-card-mzh22bq8.js.map} +1 -1
  56. package/public/assets/{startup-state-settings-card-CQaOfOwm.js → startup-state-settings-card-B1UMzsQK.js} +2 -2
  57. package/public/assets/{startup-state-settings-card-CQaOfOwm.js.map → startup-state-settings-card-B1UMzsQK.js.map} +1 -1
  58. package/public/assets/{startup-state-settings-view-Cd3LgXsC.js → startup-state-settings-view-nZNbhUhQ.js} +2 -2
  59. package/public/assets/{startup-state-settings-view-Cd3LgXsC.js.map → startup-state-settings-view-nZNbhUhQ.js.map} +1 -1
  60. package/public/assets/{terminal-square-CBQGNjjn.js → terminal-square-CdJ4Gt6Y.js} +2 -2
  61. package/public/assets/{terminal-square-CBQGNjjn.js.map → terminal-square-CdJ4Gt6Y.js.map} +1 -1
  62. package/public/assets/{tool-call-Q2b_KH6g.js → tool-call-C9ux0znU.js} +4 -4
  63. package/public/assets/{tool-call-Q2b_KH6g.js.map → tool-call-C9ux0znU.js.map} +1 -1
  64. package/public/assets/{unified-picker-Dubi2nCg.js → unified-picker-DOP29DIt.js} +2 -2
  65. package/public/assets/{unified-picker-Dubi2nCg.js.map → unified-picker-DOP29DIt.js.map} +1 -1
  66. package/public/index.html +1 -1
  67. package/public/sw.js +1 -1
  68. package/public/sw.js.map +1 -1
  69. package/public/assets/client-state-owner-commands-C_V-9FL7.js +0 -2
  70. package/public/assets/main-CSjPT-d2.js.map +0 -1
@@ -0,0 +1,97 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { bindRefs, performAction, snapshotPage } from "./page/actions";
3
+ import { RefTable } from "./page/refs";
4
+ const OPERATION_MS = 25_000;
5
+ const MAX_PENDING = 32;
6
+ const MAX_SESSIONS = 128;
7
+ /** Runs the same page engine as the app window on an owned visible native preview.
8
+ * Hosts retain target authority: every protocol call checks an exact operation lease
9
+ * and document generation. No protocol method or script is accepted from a model. */
10
+ export class BrowserPage {
11
+ constructor(native) {
12
+ this.native = native;
13
+ this.queues = new Map();
14
+ this.tables = new Map();
15
+ this.sequence = 0;
16
+ this.pending = 0;
17
+ this.closed = false;
18
+ }
19
+ async execute(sessionID, action, signal) {
20
+ if (this.closed)
21
+ throw new Error("Browser automation is shutting down");
22
+ if (this.pending >= MAX_PENDING)
23
+ throw new Error("Too many pending browser operations");
24
+ this.pending++;
25
+ const previous = this.queues.get(sessionID) ?? Promise.resolve();
26
+ const operation = previous.then(() => this.run(sessionID, action, signal));
27
+ const tail = operation.then(() => undefined, () => undefined);
28
+ this.queues.set(sessionID, tail);
29
+ try {
30
+ return await operation;
31
+ }
32
+ finally {
33
+ this.pending--;
34
+ if (this.queues.get(sessionID) === tail)
35
+ this.queues.delete(sessionID);
36
+ }
37
+ }
38
+ close() { this.closed = true; this.tables.clear(); }
39
+ async run(sessionID, action, signal) {
40
+ const operationID = randomUUID(), deadline = Date.now() + OPERATION_MS;
41
+ const current = () => {
42
+ signal?.throwIfAborted();
43
+ if (this.closed)
44
+ throw new Error("Browser automation is shutting down");
45
+ if (Date.now() >= deadline)
46
+ throw new Error("Browser operation timed out; inspect before another action");
47
+ };
48
+ const request = async (method, params) => {
49
+ current();
50
+ const result = await this.native.request(method, { sessionID, operationID, ...params }, Math.max(1, deadline - Date.now()));
51
+ current();
52
+ return result;
53
+ };
54
+ try {
55
+ const target = await request("browser.page.begin", {});
56
+ if (!target || typeof target.generation !== "string" || !target.generation || target.generation.length > 512
57
+ || typeof target.url !== "string")
58
+ throw new Error("Invalid native browser target");
59
+ const generation = target.generation;
60
+ let url = target.url;
61
+ const page = {
62
+ markAutomation: false,
63
+ channel: { send: (method, params) => request("browser.page.send", { generation, method, params: params ?? {} }) },
64
+ checkpoint: async () => {
65
+ const result = await request("browser.page.check", { generation });
66
+ if (!result || typeof result.url !== "string")
67
+ throw new Error("Invalid native browser target");
68
+ url = result.url;
69
+ },
70
+ };
71
+ const refs = this.table(sessionID);
72
+ if (action.action === "snapshot") {
73
+ const rootBackendNodeId = action.ref ? refs.resolve(action.ref, generation) : undefined;
74
+ const snapshot = await snapshotPage(page, () => refs.issuer(generation), { rootBackendNodeId, query: action.query });
75
+ return { url, snapshot: snapshot.text, collapsed: snapshot.collapsed };
76
+ }
77
+ const result = await performAction(page, bindRefs(action, ref => refs.resolve(ref, generation)));
78
+ return { url, ...result };
79
+ }
80
+ finally {
81
+ // Cleanup is exact, best effort and independent of cancellation/navigation. A lost
82
+ // reply never replays the operation; the native lease also expires on its own.
83
+ await this.native.request("browser.page.end", { sessionID, operationID }, 5_000).catch(() => undefined);
84
+ }
85
+ }
86
+ table(sessionID) {
87
+ let table = this.tables.get(sessionID);
88
+ if (!table)
89
+ table = new RefTable("take a new snapshot", `b${++this.sequence}e`);
90
+ this.tables.delete(sessionID);
91
+ this.tables.set(sessionID, table);
92
+ if (this.tables.size > MAX_SESSIONS)
93
+ this.tables.delete(this.tables.keys().next().value);
94
+ return table;
95
+ }
96
+ }
97
+ //# sourceMappingURL=browser-page.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser-page.js","sourceRoot":"","sources":["../../src/automation/browser-page.ts"],"names":[],"mappings":"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","sourcesContent":["import { randomUUID } from \"node:crypto\"\nimport type { NativeParent } from \"../native-parent\"\nimport { bindRefs, performAction, snapshotPage, type PageSession } from \"./page/actions\"\nimport type { RefAction } from \"./page/parse\"\nimport { RefTable } from \"./page/refs\"\n\nexport type BrowserPageAction = RefAction | { action: \"snapshot\"; ref?: string; query?: string }\nconst OPERATION_MS = 25_000\nconst MAX_PENDING = 32\nconst MAX_SESSIONS = 128\n\n/** Runs the same page engine as the app window on an owned visible native preview.\n * Hosts retain target authority: every protocol call checks an exact operation lease\n * and document generation. No protocol method or script is accepted from a model. */\nexport class BrowserPage {\n private readonly queues = new Map<string, Promise<void>>()\n private readonly tables = new Map<string, RefTable>()\n private sequence = 0\n private pending = 0\n private closed = false\n\n constructor(private readonly native: Pick<NativeParent, \"request\">) {}\n\n async execute(sessionID: string, action: BrowserPageAction, signal?: AbortSignal): Promise<Record<string, unknown>> {\n if (this.closed) throw new Error(\"Browser automation is shutting down\")\n if (this.pending >= MAX_PENDING) throw new Error(\"Too many pending browser operations\")\n this.pending++\n const previous = this.queues.get(sessionID) ?? Promise.resolve()\n const operation = previous.then(() => this.run(sessionID, action, signal))\n const tail = operation.then(() => undefined, () => undefined)\n this.queues.set(sessionID, tail)\n try { return await operation }\n finally {\n this.pending--\n if (this.queues.get(sessionID) === tail) this.queues.delete(sessionID)\n }\n }\n\n close(): void { this.closed = true; this.tables.clear() }\n\n private async run(sessionID: string, action: BrowserPageAction, signal?: AbortSignal): Promise<Record<string, unknown>> {\n const operationID = randomUUID(), deadline = Date.now() + OPERATION_MS\n const current = () => {\n signal?.throwIfAborted()\n if (this.closed) throw new Error(\"Browser automation is shutting down\")\n if (Date.now() >= deadline) throw new Error(\"Browser operation timed out; inspect before another action\")\n }\n const request = async <T>(method: string, params: Record<string, unknown>): Promise<T> => {\n current()\n const result = await this.native.request<T>(method, { sessionID, operationID, ...params }, Math.max(1, deadline - Date.now()))\n current()\n return result\n }\n try {\n const target = await request<{ generation: string; url: string }>(\"browser.page.begin\", {})\n if (!target || typeof target.generation !== \"string\" || !target.generation || target.generation.length > 512\n || typeof target.url !== \"string\") throw new Error(\"Invalid native browser target\")\n const generation = target.generation\n let url = target.url\n const page: PageSession = {\n markAutomation: false,\n channel: { send: (method, params) => request(\"browser.page.send\", { generation, method, params: params ?? {} }) },\n checkpoint: async () => {\n const result = await request<{ url: string }>(\"browser.page.check\", { generation })\n if (!result || typeof result.url !== \"string\") throw new Error(\"Invalid native browser target\")\n url = result.url\n },\n }\n const refs = this.table(sessionID)\n if (action.action === \"snapshot\") {\n const rootBackendNodeId = action.ref ? refs.resolve(action.ref, generation) : undefined\n const snapshot = await snapshotPage(page, () => refs.issuer(generation), { rootBackendNodeId, query: action.query })\n return { url, snapshot: snapshot.text, collapsed: snapshot.collapsed }\n }\n const result = await performAction(page, bindRefs(action, ref => refs.resolve(ref, generation)))\n return { url, ...result }\n } finally {\n // Cleanup is exact, best effort and independent of cancellation/navigation. A lost\n // reply never replays the operation; the native lease also expires on its own.\n await this.native.request(\"browser.page.end\", { sessionID, operationID }, 5_000).catch(() => undefined)\n }\n }\n\n private table(sessionID: string): RefTable {\n let table = this.tables.get(sessionID)\n if (!table) table = new RefTable(\"take a new snapshot\", `b${++this.sequence}e`)\n this.tables.delete(sessionID)\n this.tables.set(sessionID, table)\n if (this.tables.size > MAX_SESSIONS) this.tables.delete(this.tables.keys().next().value!)\n return table\n }\n}\n"]}
@@ -0,0 +1,119 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+ import { BrowserPage } from "./browser-page";
4
+ class Native {
5
+ constructor() {
6
+ this.generation = "run/guest/document-1";
7
+ this.hidden = false;
8
+ this.calls = [];
9
+ this.leases = new Map();
10
+ this.nodes = [{ nodeId: "one", backendDOMNodeId: 42, role: { value: "button" }, name: { value: "Save" } }];
11
+ this.request = async (method, input) => {
12
+ this.calls.push({ method, input });
13
+ if (method === "browser.page.end") {
14
+ this.leases.delete(input.operationID);
15
+ return {};
16
+ }
17
+ if (this.hidden)
18
+ throw new Error("No visible browser");
19
+ if (method === "browser.page.begin") {
20
+ this.leases.set(input.operationID, this.generation);
21
+ return { generation: this.generation, url: "https://example.test/" };
22
+ }
23
+ if (input.generation !== this.generation || this.leases.get(input.operationID) !== this.generation)
24
+ throw new Error("Browser document changed");
25
+ if (method === "browser.page.check")
26
+ return { url: "https://example.test/" };
27
+ if (input.method === this.failMethod)
28
+ throw new Error("Native reply uncertain");
29
+ if (input.method === "Accessibility.getFullAXTree") {
30
+ await this.gate?.();
31
+ return { nodes: this.nodes };
32
+ }
33
+ if (input.method === "DOM.resolveNode")
34
+ return { object: { objectId: "object-1" } };
35
+ if (input.method === "Runtime.callFunctionOn")
36
+ return { result: { value: null } };
37
+ return {};
38
+ };
39
+ }
40
+ }
41
+ const firstRef = (snapshot) => /\[(b\d+e\d+)\]/.exec(String(snapshot))[1];
42
+ test("preview operations use the shared outline/actions without a CodeNomad input mark, and release their exact lease", async () => {
43
+ const native = new Native(), page = new BrowserPage(native);
44
+ const first = await page.execute("session", { action: "snapshot" });
45
+ assert.match(String(first.snapshot), /- button "Save" \[b1e1\]/);
46
+ const ref = firstRef(first.snapshot);
47
+ assert.deepEqual(await page.execute("session", { action: "click", ref }), { url: "https://example.test/" });
48
+ const functions = native.calls.filter(call => call.input.method === "Runtime.callFunctionOn");
49
+ assert.ok(functions.length > 0);
50
+ assert.ok(functions.every(call => !String(call.input.params.functionDeclaration).includes("__codenomadAutomation")), "a user's web page never receives the app's input provenance variables");
51
+ assert.equal(native.leases.size, 0);
52
+ const starts = native.calls.filter(call => call.method === "browser.page.begin");
53
+ assert.equal(starts.length, 2);
54
+ assert.notEqual(starts[0].input.operationID, starts[1].input.operationID);
55
+ assert.deepEqual(native.calls.filter(call => call.method === "browser.page.end").map(call => call.input.operationID), starts.map(call => call.input.operationID));
56
+ });
57
+ test("scoped snapshots preserve refs and a new document or another session cannot reuse them", async () => {
58
+ const native = new Native(), page = new BrowserPage(native);
59
+ const ref = firstRef((await page.execute("a", { action: "snapshot" })).snapshot);
60
+ assert.match(String((await page.execute("a", { action: "snapshot", ref })).snapshot), new RegExp(`\\[${ref}\\]`));
61
+ await page.execute("b", { action: "snapshot" });
62
+ await assert.rejects(page.execute("b", { action: "click", ref }), /stale ref/);
63
+ assert.equal(firstRef((await page.execute("a", { action: "snapshot" })).snapshot), ref);
64
+ native.generation = "run/guest/document-2";
65
+ await assert.rejects(page.execute("a", { action: "click", ref }), /stale ref/);
66
+ const next = firstRef((await page.execute("a", { action: "snapshot" })).snapshot);
67
+ assert.notEqual(next, ref);
68
+ assert.equal(native.leases.size, 0);
69
+ });
70
+ test("navigation during a native read cannot publish refs, and hidden previews refuse without a fallback", async () => {
71
+ const native = new Native(), page = new BrowserPage(native);
72
+ native.gate = async () => { native.generation = "changed"; };
73
+ await assert.rejects(page.execute("a", { action: "snapshot" }), /document changed/);
74
+ assert.equal(native.leases.size, 0);
75
+ native.hidden = true;
76
+ await assert.rejects(page.execute("a", { action: "snapshot" }), /No visible browser/);
77
+ assert.ok(native.calls.every(call => call.method.startsWith("browser.page.")));
78
+ });
79
+ test("queued cancellation and shutdown perform no later input; aborted in-flight reads are fenced and cleaned up", async () => {
80
+ const native = new Native(), page = new BrowserPage(native);
81
+ let release, entered;
82
+ const started = new Promise(resolve => { entered = resolve; });
83
+ native.gate = async () => { entered(); await new Promise(resolve => { release = resolve; }); };
84
+ const abort = new AbortController();
85
+ const first = page.execute("a", { action: "snapshot" }, abort.signal);
86
+ await started;
87
+ const queuedAbort = new AbortController();
88
+ const queued = page.execute("a", { action: "press", key: "Enter" }, queuedAbort.signal);
89
+ queuedAbort.abort();
90
+ abort.abort();
91
+ release();
92
+ await assert.rejects(first, /abort/i);
93
+ await assert.rejects(queued, /abort/i);
94
+ assert.equal(native.calls.filter(call => call.method === "browser.page.begin").length, 1);
95
+ assert.equal(native.leases.size, 0);
96
+ page.close();
97
+ await assert.rejects(page.execute("a", { action: "snapshot" }), /shutting down/);
98
+ });
99
+ test("a failed native mutation is never replayed, and queue admission is bounded", async () => {
100
+ const native = new Native(), page = new BrowserPage(native);
101
+ const ref = firstRef((await page.execute("a", { action: "snapshot" })).snapshot);
102
+ native.failMethod = "Runtime.callFunctionOn";
103
+ await assert.rejects(page.execute("a", { action: "click", ref }), /reply uncertain/);
104
+ assert.equal(native.calls.filter(call => call.input.method === "Runtime.callFunctionOn").length, 1);
105
+ assert.equal(native.leases.size, 0);
106
+ native.failMethod = undefined;
107
+ let release, entered;
108
+ const started = new Promise(resolve => { entered = resolve; });
109
+ native.gate = async () => { entered(); await new Promise(resolve => { release = resolve; }); };
110
+ const first = page.execute("a", { action: "snapshot" }).catch(error => error);
111
+ await started;
112
+ const pending = Array.from({ length: 31 }, () => page.execute("a", { action: "snapshot" }).catch(error => error));
113
+ await assert.rejects(page.execute("a", { action: "snapshot" }), /Too many pending/);
114
+ page.close();
115
+ release();
116
+ assert.ok((await Promise.all([first, ...pending])).every(result => result instanceof Error));
117
+ assert.equal(native.leases.size, 0);
118
+ });
119
+ //# sourceMappingURL=browser-page.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser-page.test.js","sourceRoot":"","sources":["../../src/automation/browser-page.test.ts"],"names":[],"mappings":"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","sourcesContent":["import assert from \"node:assert/strict\"\nimport { test } from \"node:test\"\nimport { BrowserPage } from \"./browser-page\"\n\nclass Native {\n generation = \"run/guest/document-1\"\n hidden = false\n failMethod?: string\n calls: Array<{ method: string; input: Record<string, any> }> = []\n gate?: () => Promise<void>\n leases = new Map<string, string>()\n nodes = [{ nodeId: \"one\", backendDOMNodeId: 42, role: { value: \"button\" }, name: { value: \"Save\" } }]\n request = async <T>(method: string, input: Record<string, any>): Promise<T> => {\n this.calls.push({ method, input })\n if (method === \"browser.page.end\") { this.leases.delete(input.operationID); return {} as T }\n if (this.hidden) throw new Error(\"No visible browser\")\n if (method === \"browser.page.begin\") {\n this.leases.set(input.operationID, this.generation)\n return { generation: this.generation, url: \"https://example.test/\" } as T\n }\n if (input.generation !== this.generation || this.leases.get(input.operationID) !== this.generation) throw new Error(\"Browser document changed\")\n if (method === \"browser.page.check\") return { url: \"https://example.test/\" } as T\n if (input.method === this.failMethod) throw new Error(\"Native reply uncertain\")\n if (input.method === \"Accessibility.getFullAXTree\") { await this.gate?.(); return { nodes: this.nodes } as T }\n if (input.method === \"DOM.resolveNode\") return { object: { objectId: \"object-1\" } } as T\n if (input.method === \"Runtime.callFunctionOn\") return { result: { value: null } } as T\n return {} as T\n }\n}\n\nconst firstRef = (snapshot: unknown) => /\\[(b\\d+e\\d+)\\]/.exec(String(snapshot))![1]\n\ntest(\"preview operations use the shared outline/actions without a CodeNomad input mark, and release their exact lease\", async () => {\n const native = new Native(), page = new BrowserPage(native)\n const first = await page.execute(\"session\", { action: \"snapshot\" })\n assert.match(String(first.snapshot), /- button \"Save\" \\[b1e1\\]/)\n const ref = firstRef(first.snapshot)\n assert.deepEqual(await page.execute(\"session\", { action: \"click\", ref }), { url: \"https://example.test/\" })\n const functions = native.calls.filter(call => call.input.method === \"Runtime.callFunctionOn\")\n assert.ok(functions.length > 0)\n assert.ok(functions.every(call => !String(call.input.params.functionDeclaration).includes(\"__codenomadAutomation\")),\n \"a user's web page never receives the app's input provenance variables\")\n assert.equal(native.leases.size, 0)\n const starts = native.calls.filter(call => call.method === \"browser.page.begin\")\n assert.equal(starts.length, 2)\n assert.notEqual(starts[0].input.operationID, starts[1].input.operationID)\n assert.deepEqual(native.calls.filter(call => call.method === \"browser.page.end\").map(call => call.input.operationID), starts.map(call => call.input.operationID))\n})\n\ntest(\"scoped snapshots preserve refs and a new document or another session cannot reuse them\", async () => {\n const native = new Native(), page = new BrowserPage(native)\n const ref = firstRef((await page.execute(\"a\", { action: \"snapshot\" })).snapshot)\n assert.match(String((await page.execute(\"a\", { action: \"snapshot\", ref })).snapshot), new RegExp(`\\\\[${ref}\\\\]`))\n await page.execute(\"b\", { action: \"snapshot\" })\n await assert.rejects(page.execute(\"b\", { action: \"click\", ref }), /stale ref/)\n assert.equal(firstRef((await page.execute(\"a\", { action: \"snapshot\" })).snapshot), ref)\n native.generation = \"run/guest/document-2\"\n await assert.rejects(page.execute(\"a\", { action: \"click\", ref }), /stale ref/)\n const next = firstRef((await page.execute(\"a\", { action: \"snapshot\" })).snapshot)\n assert.notEqual(next, ref)\n assert.equal(native.leases.size, 0)\n})\n\ntest(\"navigation during a native read cannot publish refs, and hidden previews refuse without a fallback\", async () => {\n const native = new Native(), page = new BrowserPage(native)\n native.gate = async () => { native.generation = \"changed\" }\n await assert.rejects(page.execute(\"a\", { action: \"snapshot\" }), /document changed/)\n assert.equal(native.leases.size, 0)\n native.hidden = true\n await assert.rejects(page.execute(\"a\", { action: \"snapshot\" }), /No visible browser/)\n assert.ok(native.calls.every(call => call.method.startsWith(\"browser.page.\")))\n})\n\ntest(\"queued cancellation and shutdown perform no later input; aborted in-flight reads are fenced and cleaned up\", async () => {\n const native = new Native(), page = new BrowserPage(native)\n let release!: () => void, entered!: () => void\n const started = new Promise<void>(resolve => { entered = resolve })\n native.gate = async () => { entered(); await new Promise<void>(resolve => { release = resolve }) }\n const abort = new AbortController()\n const first = page.execute(\"a\", { action: \"snapshot\" }, abort.signal)\n await started\n const queuedAbort = new AbortController()\n const queued = page.execute(\"a\", { action: \"press\", key: \"Enter\" }, queuedAbort.signal)\n queuedAbort.abort()\n abort.abort()\n release()\n await assert.rejects(first, /abort/i)\n await assert.rejects(queued, /abort/i)\n assert.equal(native.calls.filter(call => call.method === \"browser.page.begin\").length, 1)\n assert.equal(native.leases.size, 0)\n page.close()\n await assert.rejects(page.execute(\"a\", { action: \"snapshot\" }), /shutting down/)\n})\n\ntest(\"a failed native mutation is never replayed, and queue admission is bounded\", async () => {\n const native = new Native(), page = new BrowserPage(native)\n const ref = firstRef((await page.execute(\"a\", { action: \"snapshot\" })).snapshot)\n native.failMethod = \"Runtime.callFunctionOn\"\n await assert.rejects(page.execute(\"a\", { action: \"click\", ref }), /reply uncertain/)\n assert.equal(native.calls.filter(call => call.input.method === \"Runtime.callFunctionOn\").length, 1)\n assert.equal(native.leases.size, 0)\n native.failMethod = undefined\n let release!: () => void, entered!: () => void\n const started = new Promise<void>(resolve => { entered = resolve })\n native.gate = async () => { entered(); await new Promise<void>(resolve => { release = resolve }) }\n const first = page.execute(\"a\", { action: \"snapshot\" }).catch(error => error)\n await started\n const pending = Array.from({ length: 31 }, () => page.execute(\"a\", { action: \"snapshot\" }).catch(error => error))\n await assert.rejects(page.execute(\"a\", { action: \"snapshot\" }), /Too many pending/)\n page.close()\n release()\n assert.ok((await Promise.all([first, ...pending])).every(result => result instanceof Error))\n assert.equal(native.leases.size, 0)\n})\n"]}
@@ -1,6 +1,20 @@
1
+ import { aim, clickAt, dragBetween, hoverAt, pressKeys, waitForText, wheelAt } from "./input";
2
+ import { callOn, markedInput, OBJECT_GROUP, releaseObjects, resolveElement, VALUE_LIMIT } from "./protocol";
1
3
  import { renderSnapshot } from "./snapshot";
2
- const OBJECT_GROUP = "codenomad-automation";
3
- const VALUE_LIMIT = 200;
4
+ /** Resolves a tool's refs to DOM nodes before any page access; a stale ref fails the whole action. */
5
+ export function bindRefs(action, resolve) {
6
+ const target = (ref) => ({ ref, backendNodeId: resolve(ref) });
7
+ switch (action.action) {
8
+ case "click": return { kind: "click", ...target(action.ref), ...(action.button ? { button: action.button } : {}) };
9
+ case "type": return { kind: "type", ...target(action.ref), text: action.text, clear: action.clear };
10
+ case "select": return { kind: "select", ...target(action.ref), values: action.values };
11
+ case "hover": return { kind: "hover", ...target(action.ref) };
12
+ case "drag": return { kind: "drag", ...target(action.ref), to: target(action.to) };
13
+ case "scroll": return { kind: "scroll", direction: action.direction, amount: action.amount, ...(action.ref ? { target: target(action.ref) } : {}) };
14
+ case "press": return { kind: "press", key: action.key, ...(action.ref ? { target: target(action.ref) } : {}) };
15
+ case "wait": return { kind: "wait", text: action.text, gone: action.gone, timeoutMs: action.timeout };
16
+ }
17
+ }
4
18
  export async function readAccessibilityTree(session) {
5
19
  const result = await session.channel.send("Accessibility.getFullAXTree");
6
20
  await session.checkpoint();
@@ -12,94 +26,93 @@ export async function snapshotPage(session, issue, options = {}) {
12
26
  return renderSnapshot(nodes, issue(), options);
13
27
  }
14
28
  export async function performAction(session, action) {
15
- const { channel } = session;
16
29
  try {
17
- await channel.send("DOM.scrollIntoViewIfNeeded", { backendNodeId: action.backendNodeId }).catch(() => undefined);
18
- await session.checkpoint();
19
- const element = await resolve(session, action);
20
- if (action.kind === "click") {
21
- const selected = await call(session, action, element, CLICK, []);
22
- return Array.isArray(selected) ? { selected: selected.map(String) } : {};
23
- }
24
- if (action.kind === "select") {
25
- return { selected: (await call(session, action, element, SELECT, [action.values])).map(String) };
26
- }
27
- const field = await call(session, action, element, FIELD, [], false);
28
- const mode = await call(session, action, field, PREPARE, [action.clear]);
29
- if (mode === "value" || mode === "append")
30
- await call(session, action, field, SET_VALUE, [action.text, mode === "append"]);
31
- else if (action.text) {
32
- await channel.send("Input.insertText", { text: action.text });
33
- await session.checkpoint();
30
+ switch (action.kind) {
31
+ case "click": return await click(session, action);
32
+ case "type": return await type(session, action);
33
+ case "select": {
34
+ const element = await resolveElement(session, action.ref, action.backendNodeId);
35
+ return { selected: (await callOn(session, action.ref, element, SELECT, [action.values])).map(String) };
36
+ }
37
+ case "hover": {
38
+ const point = await aim(session, action.ref, action.backendNodeId);
39
+ if (!point)
40
+ throw new Error(`${action.ref} has no visible box to hover`);
41
+ await hoverAt(session, point);
42
+ return {};
43
+ }
44
+ case "drag": {
45
+ // Both ends must already be visible: scrolling to the second target would
46
+ // invalidate the first point and could press a different row underneath it.
47
+ const from = await aim(session, action.ref, action.backendNodeId, { scroll: false });
48
+ const to = await aim(session, action.to.ref, action.to.backendNodeId, { scroll: false });
49
+ if (!from || !to)
50
+ throw new Error(`${!from ? action.ref : action.to.ref} has no visible box to drag ${!from ? "from" : "to"}`);
51
+ await dragBetween(session, from, to);
52
+ return {};
53
+ }
54
+ case "scroll": {
55
+ const point = action.target ? await aim(session, action.target.ref, action.target.backendNodeId) : undefined;
56
+ if (action.target && !point)
57
+ throw new Error(`${action.target.ref} has no visible box to scroll`);
58
+ const sign = action.direction === "up" || action.direction === "left" ? -1 : 1;
59
+ const vertical = action.direction === "up" || action.direction === "down";
60
+ await wheelAt(session, point, { x: vertical ? 0 : sign * action.amount, y: vertical ? sign * action.amount : 0 });
61
+ return {};
62
+ }
63
+ case "press": {
64
+ if (action.target) {
65
+ const element = await resolveElement(session, action.target.ref, action.target.backendNodeId);
66
+ await callOn(session, action.target.ref, element, FOCUS, []);
67
+ }
68
+ await pressKeys(session, action.key);
69
+ return { focused: await focusedElement(session) };
70
+ }
71
+ case "wait": return await waitForText(session, action.text, action.gone, action.timeoutMs);
34
72
  }
35
- else if (action.clear)
36
- await call(session, action, field, DELETE_SELECTION, []);
37
- return { value: String(await call(session, action, field, READ_VALUE, [])) };
38
73
  }
39
74
  finally {
40
- await channel.send("Runtime.releaseObjectGroup", { objectGroup: OBJECT_GROUP }).catch(() => undefined);
75
+ await releaseObjects(session);
41
76
  }
42
77
  }
43
- async function resolve(session, action) {
44
- let result;
45
- try {
46
- result = await session.channel.send("DOM.resolveNode", { backendNodeId: action.backendNodeId, objectGroup: OBJECT_GROUP });
47
- }
48
- catch {
49
- throw new Error(`${action.ref} is no longer in the page; inspect again`);
78
+ async function click(session, action) {
79
+ const element = await resolveElement(session, action.ref, action.backendNodeId);
80
+ const selected = await callOn(session, action.ref, element, OPTION_CLICK, []);
81
+ if (Array.isArray(selected))
82
+ return { selected: selected.map(String) };
83
+ const point = await aim(session, action.ref, action.backendNodeId);
84
+ // An element without a visible box (zero-size or offscreen-only) is activated programmatically.
85
+ if (!point) {
86
+ if (action.button && action.button !== "left")
87
+ throw new Error(`${action.ref} has no visible box to ${action.button}-click`);
88
+ await callOn(session, action.ref, element, PROGRAMMATIC_CLICK, []);
50
89
  }
51
- await session.checkpoint();
52
- const objectId = result.object?.objectId;
53
- if (typeof objectId !== "string")
54
- throw new Error(`${action.ref} is no longer in the page; inspect again`);
55
- return objectId;
90
+ else
91
+ await clickAt(session, point, action.button);
92
+ return {};
56
93
  }
57
- /** Runs a fixed page function on an element; `byValue: false` returns another element handle. */
58
- async function call(session, action, objectId, body, args, byValue = true) {
59
- const result = await session.channel.send("Runtime.callFunctionOn", {
60
- objectId,
61
- functionDeclaration: pageFunction(body, session.markAutomation),
62
- arguments: args.map(value => ({ value })),
63
- returnByValue: byValue,
64
- objectGroup: OBJECT_GROUP,
65
- });
94
+ async function type(session, action) {
95
+ const element = await resolveElement(session, action.ref, action.backendNodeId);
96
+ await session.channel.send("DOM.scrollIntoViewIfNeeded", { backendNodeId: action.backendNodeId }).catch(() => undefined);
66
97
  await session.checkpoint();
67
- const failure = result.exceptionDetails;
68
- if (failure)
69
- throw pageError(action, String(failure.exception?.description ?? failure.text ?? ""));
70
- const value = result.result;
71
- if (!byValue) {
72
- if (typeof value?.objectId !== "string")
73
- throw new Error(`${action.ref} is no longer in the page; inspect again`);
74
- return value.objectId;
75
- }
76
- return value?.value;
77
- }
78
- function pageFunction(body, mark) {
79
- const element = `const element = this.nodeType === Node.ELEMENT_NODE ? this : this.parentElement;
80
- if (!(element instanceof Element) || !element.isConnected) throw new Error("codenomad:gone");`;
81
- return mark
82
- ? `function (...args) { ${element} globalThis.__codenomadAutomationDepth = (globalThis.__codenomadAutomationDepth || 0) + 1;
83
- try { return (() => { ${body} })(); } finally { globalThis.__codenomadAutomationDepth--; } }`
84
- : `function (...args) { ${element} ${body} }`;
98
+ const field = await callOn(session, action.ref, element, FIELD, [], false);
99
+ const mode = await callOn(session, action.ref, field, PREPARE, [action.clear]);
100
+ if (mode === "value" || mode === "append")
101
+ await callOn(session, action.ref, field, SET_VALUE, [action.text, mode === "append"]);
102
+ else if (action.text)
103
+ await markedInput(session, dispatch => dispatch("Input.insertText", { text: action.text }));
104
+ else if (action.clear)
105
+ await callOn(session, action.ref, field, DELETE_SELECTION, []);
106
+ return { value: String(await callOn(session, action.ref, field, READ_VALUE, [])) };
85
107
  }
86
- function pageError(action, description) {
87
- const [code, detail = ""] = /codenomad:([a-z-]+)(?::(.*))?/.exec(description.split("\n", 1)[0])?.slice(1) ?? [];
88
- const quoted = JSON.stringify(detail.slice(0, VALUE_LIMIT));
89
- switch (code) {
90
- case "gone": return new Error(`${action.ref} is no longer in the page; inspect again`);
91
- case "not-select": return new Error(`${action.ref} is not a native select; click it to open its menu, then click the option`);
92
- case "no-option": return new Error(`No option ${quoted} in ${action.ref}; inspect it to see its options`);
93
- case "option-disabled": return new Error(`Option ${quoted} of ${action.ref} is disabled`);
94
- case "single": return new Error(`${action.ref} takes exactly one value`);
95
- case "disabled": return new Error(`${action.ref} is disabled`);
96
- case "readonly": return new Error(`${action.ref} is read-only`);
97
- case "not-editable": return new Error(`${action.ref} is not a text field`);
98
- case "not-text": return new Error(`${action.ref} is not a text field; use click`);
99
- case "unfocusable": return new Error(`${action.ref} cannot take focus (hidden, disabled or behind a dialog); reveal it, then inspect again`);
100
- case "invalid-value": return new Error(`${quoted} is not a valid value for ${action.ref}`);
101
- default: return new Error(`The page rejected the action on ${action.ref}: ${description.split("\n", 1)[0].slice(0, VALUE_LIMIT)}`);
102
- }
108
+ async function focusedElement(session) {
109
+ const active = await session.channel.send("Runtime.evaluate", { expression: "document.activeElement", objectGroup: OBJECT_GROUP });
110
+ await session.checkpoint();
111
+ const objectId = active.result?.objectId;
112
+ if (typeof objectId !== "string")
113
+ return undefined;
114
+ const described = await callOn(session, "the focused element", objectId, DESCRIBE, []);
115
+ return typeof described === "string" ? described : undefined;
103
116
  }
104
117
  const LABEL = `const label = option => (option.label || option.textContent || "").replace(/\\s+/g, " ").trim();`;
105
118
  /** Picks options like a user would: by visible label, else by value; change fires only on a change.
@@ -129,11 +142,14 @@ const CHOOSE = `${LABEL}
129
142
  }
130
143
  return apply(select, picked);
131
144
  };`;
132
- /** Clicking an option of a native select picks that exact option, since its popup is not part of the page. */
133
- const CLICK = `${CHOOSE}
145
+ /** A native select's popup is not part of the page: clicking an option picks that exact option,
146
+ * and the select itself is changed with `select` rather than opened. */
147
+ const OPTION_CLICK = `${CHOOSE}
134
148
  const option = element.closest("option"), select = option && option.closest("select");
135
149
  if (option && select) return apply(select, [option]);
136
- if (element instanceof HTMLElement) element.click();
150
+ if (element instanceof HTMLSelectElement) throw new Error("codenomad:is-select");
151
+ return null;`;
152
+ const PROGRAMMATIC_CLICK = `if (element instanceof HTMLElement) element.click();
137
153
  else element.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true, composed: true }));
138
154
  return null;`;
139
155
  const SELECT = `${CHOOSE}
@@ -141,6 +157,14 @@ const SELECT = `${CHOOSE}
141
157
  const select = control instanceof HTMLSelectElement ? control : control && control.closest("select");
142
158
  if (!select) throw new Error("codenomad:not-select");
143
159
  return choose(select, args[0]);`;
160
+ const FOCUS = `if (element.matches(":disabled")) throw new Error("codenomad:disabled");
161
+ if (element instanceof HTMLElement || element instanceof SVGElement) element.focus();
162
+ if (element.getRootNode().activeElement !== element) throw new Error("codenomad:unfocusable");
163
+ return null;`;
164
+ const DESCRIBE = `const role = element.getAttribute("role") || element.tagName.toLowerCase();
165
+ const name = (element.getAttribute("aria-label") || element.getAttribute("title") || element.getAttribute("placeholder")
166
+ || (element.labels && element.labels[0] && element.labels[0].textContent) || element.textContent || "").replace(/\\s+/g, " ").trim().slice(0, 80);
167
+ return role + (name ? " " + JSON.stringify(name) : "");`;
144
168
  const NOT_TEXT = ["checkbox", "radio", "button", "submit", "reset", "file", "image", "hidden"];
145
169
  const VALUE_TYPES = ["date", "time", "datetime-local", "month", "week", "range", "color"];
146
170
  /** Types whose value sanitization empties an invalid value. */
@@ -1 +1 @@
1
- {"version":3,"file":"actions.js","sourceRoot":"","sources":["../../../src/automation/page/actions.ts"],"names":[],"mappings":"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","sourcesContent":["import { renderSnapshot, type AxNode, type Snapshot, type SnapshotOptions } from \"./snapshot\"\n\n/** One page's protocol connection, whatever carries it (DevTools socket or a native pipe). */\nexport interface PageChannel {\n send(method: string, params?: Record<string, unknown>): Promise<Record<string, unknown>>\n}\n\nexport interface PageSession {\n channel: PageChannel\n /** Throws once the page changed since the operation began; runs between protocol steps. */\n checkpoint(): Promise<void>\n /** The trusted CodeNomad window: page-side automation is marked so it never counts as a\n * human answer (`form-answer-interaction.ts`). Never set this in a user's web page. */\n markAutomation: boolean\n}\n\nexport type PageAction =\n | { kind: \"click\"; ref: string; backendNodeId: number }\n | { kind: \"type\"; ref: string; backendNodeId: number; text: string; clear: boolean }\n | { kind: \"select\"; ref: string; backendNodeId: number; values: string[] }\n\nexport interface PageActionResult {\n /** The field's value after typing; passwords are masked. */\n value?: string\n /** The select's chosen option labels. */\n selected?: string[]\n}\n\nconst OBJECT_GROUP = \"codenomad-automation\"\nconst VALUE_LIMIT = 200\n\nexport async function readAccessibilityTree(session: PageSession): Promise<AxNode[]> {\n const result = await session.channel.send(\"Accessibility.getFullAXTree\")\n await session.checkpoint()\n return Array.isArray(result.nodes) ? result.nodes as AxNode[] : []\n}\n\nexport async function snapshotPage(session: PageSession, issue: () => (backendNodeId: number) => string,\n options: SnapshotOptions = {}): Promise<Snapshot> {\n const nodes = await readAccessibilityTree(session)\n // Refs are issued only after the tree was confirmed current.\n return renderSnapshot(nodes, issue(), options)\n}\n\nexport async function performAction(session: PageSession, action: PageAction): Promise<PageActionResult> {\n const { channel } = session\n try {\n await channel.send(\"DOM.scrollIntoViewIfNeeded\", { backendNodeId: action.backendNodeId }).catch(() => undefined)\n await session.checkpoint()\n const element = await resolve(session, action)\n if (action.kind === \"click\") {\n const selected = await call(session, action, element, CLICK, [])\n return Array.isArray(selected) ? { selected: selected.map(String) } : {}\n }\n if (action.kind === \"select\") {\n return { selected: (await call(session, action, element, SELECT, [action.values]) as unknown[]).map(String) }\n }\n const field = await call(session, action, element, FIELD, [], false) as string\n const mode = await call(session, action, field, PREPARE, [action.clear])\n if (mode === \"value\" || mode === \"append\") await call(session, action, field, SET_VALUE, [action.text, mode === \"append\"])\n else if (action.text) {\n await channel.send(\"Input.insertText\", { text: action.text })\n await session.checkpoint()\n } else if (action.clear) await call(session, action, field, DELETE_SELECTION, [])\n return { value: String(await call(session, action, field, READ_VALUE, [])) }\n } finally {\n await channel.send(\"Runtime.releaseObjectGroup\", { objectGroup: OBJECT_GROUP }).catch(() => undefined)\n }\n}\n\nasync function resolve(session: PageSession, action: PageAction): Promise<string> {\n let result: Record<string, unknown>\n try {\n result = await session.channel.send(\"DOM.resolveNode\", { backendNodeId: action.backendNodeId, objectGroup: OBJECT_GROUP })\n } catch {\n throw new Error(`${action.ref} is no longer in the page; inspect again`)\n }\n await session.checkpoint()\n const objectId = (result.object as { objectId?: unknown } | undefined)?.objectId\n if (typeof objectId !== \"string\") throw new Error(`${action.ref} is no longer in the page; inspect again`)\n return objectId\n}\n\n/** Runs a fixed page function on an element; `byValue: false` returns another element handle. */\nasync function call(session: PageSession, action: PageAction, objectId: string, body: string, args: unknown[], byValue = true): Promise<unknown> {\n const result = await session.channel.send(\"Runtime.callFunctionOn\", {\n objectId,\n functionDeclaration: pageFunction(body, session.markAutomation),\n arguments: args.map(value => ({ value })),\n returnByValue: byValue,\n objectGroup: OBJECT_GROUP,\n })\n await session.checkpoint()\n const failure = result.exceptionDetails as { exception?: { description?: unknown }; text?: unknown } | undefined\n if (failure) throw pageError(action, String(failure.exception?.description ?? failure.text ?? \"\"))\n const value = result.result as { value?: unknown; objectId?: unknown } | undefined\n if (!byValue) {\n if (typeof value?.objectId !== \"string\") throw new Error(`${action.ref} is no longer in the page; inspect again`)\n return value.objectId\n }\n return value?.value\n}\n\nfunction pageFunction(body: string, mark: boolean): string {\n const element = `const element = this.nodeType === Node.ELEMENT_NODE ? this : this.parentElement;\n if (!(element instanceof Element) || !element.isConnected) throw new Error(\"codenomad:gone\");`\n return mark\n ? `function (...args) { ${element} globalThis.__codenomadAutomationDepth = (globalThis.__codenomadAutomationDepth || 0) + 1;\n try { return (() => { ${body} })(); } finally { globalThis.__codenomadAutomationDepth--; } }`\n : `function (...args) { ${element} ${body} }`\n}\n\nfunction pageError(action: PageAction, description: string): Error {\n const [code, detail = \"\"] = /codenomad:([a-z-]+)(?::(.*))?/.exec(description.split(\"\\n\", 1)[0])?.slice(1) ?? []\n const quoted = JSON.stringify(detail.slice(0, VALUE_LIMIT))\n switch (code) {\n case \"gone\": return new Error(`${action.ref} is no longer in the page; inspect again`)\n case \"not-select\": return new Error(`${action.ref} is not a native select; click it to open its menu, then click the option`)\n case \"no-option\": return new Error(`No option ${quoted} in ${action.ref}; inspect it to see its options`)\n case \"option-disabled\": return new Error(`Option ${quoted} of ${action.ref} is disabled`)\n case \"single\": return new Error(`${action.ref} takes exactly one value`)\n case \"disabled\": return new Error(`${action.ref} is disabled`)\n case \"readonly\": return new Error(`${action.ref} is read-only`)\n case \"not-editable\": return new Error(`${action.ref} is not a text field`)\n case \"not-text\": return new Error(`${action.ref} is not a text field; use click`)\n case \"unfocusable\": return new Error(`${action.ref} cannot take focus (hidden, disabled or behind a dialog); reveal it, then inspect again`)\n case \"invalid-value\": return new Error(`${quoted} is not a valid value for ${action.ref}`)\n default: return new Error(`The page rejected the action on ${action.ref}: ${description.split(\"\\n\", 1)[0].slice(0, VALUE_LIMIT)}`)\n }\n}\n\nconst LABEL = `const label = option => (option.label || option.textContent || \"\").replace(/\\\\s+/g, \" \").trim();`\n\n/** Picks options like a user would: by visible label, else by value; change fires only on a change.\n * `:disabled` also covers disabled fieldsets and optgroups. */\nconst CHOOSE = `${LABEL}\n const apply = (select, picked) => {\n if (select.matches(\":disabled\")) throw new Error(\"codenomad:disabled\");\n for (const option of picked) if (option.matches(\":disabled\")) throw new Error(\"codenomad:option-disabled:\" + label(option));\n if (!select.multiple && picked.length !== 1) throw new Error(\"codenomad:single\");\n select.focus();\n const options = Array.from(select.options);\n if (options.some(option => option.selected !== picked.includes(option))) {\n for (const option of options) option.selected = picked.includes(option);\n select.dispatchEvent(new Event(\"input\", { bubbles: true, composed: true }));\n select.dispatchEvent(new Event(\"change\", { bubbles: true }));\n }\n return Array.from(select.selectedOptions).map(label);\n };\n const choose = (select, wanted) => {\n if (select.matches(\":disabled\")) throw new Error(\"codenomad:disabled\");\n const options = Array.from(select.options), picked = [];\n for (const raw of wanted) {\n const target = String(raw).replace(/\\\\s+/g, \" \").trim();\n const option = options.find(item => label(item) === target) || options.find(item => item.value === String(raw));\n if (!option) throw new Error(\"codenomad:no-option:\" + target);\n if (!picked.includes(option)) picked.push(option);\n }\n return apply(select, picked);\n };`\n\n/** Clicking an option of a native select picks that exact option, since its popup is not part of the page. */\nconst CLICK = `${CHOOSE}\n const option = element.closest(\"option\"), select = option && option.closest(\"select\");\n if (option && select) return apply(select, [option]);\n if (element instanceof HTMLElement) element.click();\n else element.dispatchEvent(new MouseEvent(\"click\", { bubbles: true, cancelable: true, composed: true }));\n return null;`\n\nconst SELECT = `${CHOOSE}\n const control = element instanceof HTMLLabelElement ? element.control : element;\n const select = control instanceof HTMLSelectElement ? control : control && control.closest(\"select\");\n if (!select) throw new Error(\"codenomad:not-select\");\n return choose(select, args[0]);`\n\nconst NOT_TEXT = [\"checkbox\", \"radio\", \"button\", \"submit\", \"reset\", \"file\", \"image\", \"hidden\"]\nconst VALUE_TYPES = [\"date\", \"time\", \"datetime-local\", \"month\", \"week\", \"range\", \"color\"]\n/** Types whose value sanitization empties an invalid value. */\nconst PROBED_TYPES = [\"date\", \"time\", \"datetime-local\", \"month\", \"week\", \"number\"]\nconst TEXT_FIELD = `const textField = item => item instanceof HTMLInputElement || item instanceof HTMLTextAreaElement\n || (item instanceof HTMLElement && item.isContentEditable);`\n\n/** The field a ref designates: itself, its labelled control, or its first editable descendant. */\nconst FIELD = `${TEXT_FIELD}\n const control = element instanceof HTMLLabelElement ? element.control : element;\n const field = control && textField(control) ? control\n : element.querySelector('input:not([type=\"hidden\"]), textarea, [contenteditable]:not([contenteditable=\"false\"])');\n if (!field || !textField(field)) throw new Error(\"codenomad:not-editable\");\n return field;`\n\n/** Native text insertion goes to the focused element, so a field that cannot take focus\n * (hidden, disabled, behind a modal dialog) fails here instead of typing into another one. */\nconst PREPARE = `const field = element, clear = args[0];\n if (field.matches(\":disabled\")) throw new Error(\"codenomad:disabled\");\n if (field.readOnly) throw new Error(\"codenomad:readonly\");\n if (field instanceof HTMLInputElement && ${JSON.stringify(NOT_TEXT)}.includes(field.type)) throw new Error(\"codenomad:not-text\");\n field.focus();\n const active = field.getRootNode().activeElement;\n const editable = !(field instanceof HTMLInputElement || field instanceof HTMLTextAreaElement);\n if (active !== field && !(editable && active instanceof HTMLElement && active.isContentEditable && active.contains(field))) {\n throw new Error(\"codenomad:unfocusable\");\n }\n if (field instanceof HTMLInputElement && ${JSON.stringify(VALUE_TYPES)}.includes(field.type)) return \"value\";\n if (editable) {\n const range = document.createRange();\n range.selectNodeContents(field);\n if (!clear) range.collapse(false);\n const selection = getSelection();\n selection.removeAllRanges();\n selection.addRange(range);\n } else if (clear) field.select();\n // Email and number inputs have no caret API: their text is appended as a value.\n else { try { field.setSelectionRange(field.value.length, field.value.length); } catch { return \"append\"; } }\n return \"insert\";`\n\n/** Sets an input's value like a picker would. An invalid value is refused before anything changes;\n * a valid one keeps the browser's normalization (lowercase colors, snapped ranges). */\nconst SET_VALUE = `const [text, append] = args, next = append ? element.value + text : text;\n // Ranges and colors have no empty value: the browser would substitute its default.\n const valid = element.type === \"color\" ? /^#[0-9a-f]{6}$/i.test(next)\n : element.type === \"range\" ? /^-?(?:\\\\d+(?:\\\\.\\\\d+)?|\\\\.\\\\d+)(?:[eE][+-]?\\\\d+)?$/.test(next)\n : next !== \"\" && ${JSON.stringify(PROBED_TYPES)}.includes(element.type) ? (() => { const probe = document.createElement(\"input\"); probe.type = element.type; probe.value = next; return probe.value !== \"\"; })()\n : true;\n if (!valid) throw new Error(\"codenomad:invalid-value:\" + next);\n if (next === element.value) return null;\n Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, \"value\").set.call(element, next);\n element.dispatchEvent(new Event(\"input\", { bubbles: true, composed: true }));\n element.dispatchEvent(new Event(\"change\", { bubbles: true }));\n return null;`\n\nconst DELETE_SELECTION = `document.execCommand(\"delete\"); return null;`\n\nconst READ_VALUE = `const value = element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement ? element.value : element.innerText || element.textContent || \"\";\n if (element instanceof HTMLInputElement && element.type === \"password\") return \"•\".repeat(Math.min(value.length, 16));\n return value.length > ${VALUE_LIMIT} ? value.slice(0, ${VALUE_LIMIT - 1}) + \"…\" : value;`\n"]}
1
+ {"version":3,"file":"actions.js","sourceRoot":"","sources":["../../../src/automation/page/actions.ts"],"names":[],"mappings":"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","sourcesContent":["import { aim, clickAt, dragBetween, hoverAt, pressKeys, waitForText, wheelAt, type MouseButton } from \"./input\"\nimport { callOn, markedInput, OBJECT_GROUP, releaseObjects, resolveElement, VALUE_LIMIT, type PageSession } from \"./protocol\"\nimport type { RefAction } from \"./parse\"\nimport { renderSnapshot, type AxNode, type Snapshot, type SnapshotOptions } from \"./snapshot\"\n\nexport type { PageChannel, PageSession } from \"./protocol\"\n\ninterface Target { ref: string; backendNodeId: number }\n\nexport type PageAction =\n | Target & { kind: \"click\"; button?: MouseButton }\n | Target & { kind: \"type\"; text: string; clear: boolean }\n | Target & { kind: \"select\"; values: string[] }\n | Target & { kind: \"hover\" }\n | Target & { kind: \"drag\"; to: Target }\n | { kind: \"scroll\"; target?: Target; direction: \"up\" | \"down\" | \"left\" | \"right\"; amount: number }\n | { kind: \"press\"; key: string; target?: Target }\n | { kind: \"wait\"; text: string; gone: boolean; timeoutMs: number }\n\nexport interface PageActionResult {\n /** The field's value after typing; passwords are masked. */\n value?: string\n /** The select's chosen option labels. */\n selected?: string[]\n /** The element focused after a key press. */\n focused?: string\n /** Whether wait found its text, and after how long. */\n found?: boolean\n waitedMs?: number\n}\n\n/** Resolves a tool's refs to DOM nodes before any page access; a stale ref fails the whole action. */\nexport function bindRefs(action: RefAction, resolve: (ref: string) => number): PageAction {\n const target = (ref: string): Target => ({ ref, backendNodeId: resolve(ref) })\n switch (action.action) {\n case \"click\": return { kind: \"click\", ...target(action.ref), ...(action.button ? { button: action.button } : {}) }\n case \"type\": return { kind: \"type\", ...target(action.ref), text: action.text, clear: action.clear }\n case \"select\": return { kind: \"select\", ...target(action.ref), values: action.values }\n case \"hover\": return { kind: \"hover\", ...target(action.ref) }\n case \"drag\": return { kind: \"drag\", ...target(action.ref), to: target(action.to) }\n case \"scroll\": return { kind: \"scroll\", direction: action.direction, amount: action.amount, ...(action.ref ? { target: target(action.ref) } : {}) }\n case \"press\": return { kind: \"press\", key: action.key, ...(action.ref ? { target: target(action.ref) } : {}) }\n case \"wait\": return { kind: \"wait\", text: action.text, gone: action.gone, timeoutMs: action.timeout }\n }\n}\n\nexport async function readAccessibilityTree(session: PageSession): Promise<AxNode[]> {\n const result = await session.channel.send(\"Accessibility.getFullAXTree\")\n await session.checkpoint()\n return Array.isArray(result.nodes) ? result.nodes as AxNode[] : []\n}\n\nexport async function snapshotPage(session: PageSession, issue: () => (backendNodeId: number) => string,\n options: SnapshotOptions = {}): Promise<Snapshot> {\n const nodes = await readAccessibilityTree(session)\n // Refs are issued only after the tree was confirmed current.\n return renderSnapshot(nodes, issue(), options)\n}\n\nexport async function performAction(session: PageSession, action: PageAction): Promise<PageActionResult> {\n try {\n switch (action.kind) {\n case \"click\": return await click(session, action)\n case \"type\": return await type(session, action)\n case \"select\": {\n const element = await resolveElement(session, action.ref, action.backendNodeId)\n return { selected: (await callOn(session, action.ref, element, SELECT, [action.values]) as unknown[]).map(String) }\n }\n case \"hover\": {\n const point = await aim(session, action.ref, action.backendNodeId)\n if (!point) throw new Error(`${action.ref} has no visible box to hover`)\n await hoverAt(session, point)\n return {}\n }\n case \"drag\": {\n // Both ends must already be visible: scrolling to the second target would\n // invalidate the first point and could press a different row underneath it.\n const from = await aim(session, action.ref, action.backendNodeId, { scroll: false })\n const to = await aim(session, action.to.ref, action.to.backendNodeId, { scroll: false })\n if (!from || !to) throw new Error(`${!from ? action.ref : action.to.ref} has no visible box to drag ${!from ? \"from\" : \"to\"}`)\n await dragBetween(session, from, to)\n return {}\n }\n case \"scroll\": {\n const point = action.target ? await aim(session, action.target.ref, action.target.backendNodeId) : undefined\n if (action.target && !point) throw new Error(`${action.target.ref} has no visible box to scroll`)\n const sign = action.direction === \"up\" || action.direction === \"left\" ? -1 : 1\n const vertical = action.direction === \"up\" || action.direction === \"down\"\n await wheelAt(session, point, { x: vertical ? 0 : sign * action.amount, y: vertical ? sign * action.amount : 0 })\n return {}\n }\n case \"press\": {\n if (action.target) {\n const element = await resolveElement(session, action.target.ref, action.target.backendNodeId)\n await callOn(session, action.target.ref, element, FOCUS, [])\n }\n await pressKeys(session, action.key)\n return { focused: await focusedElement(session) }\n }\n case \"wait\": return await waitForText(session, action.text, action.gone, action.timeoutMs)\n }\n } finally {\n await releaseObjects(session)\n }\n}\n\nasync function click(session: PageSession, action: Target & { button?: MouseButton }): Promise<PageActionResult> {\n const element = await resolveElement(session, action.ref, action.backendNodeId)\n const selected = await callOn(session, action.ref, element, OPTION_CLICK, [])\n if (Array.isArray(selected)) return { selected: selected.map(String) }\n const point = await aim(session, action.ref, action.backendNodeId)\n // An element without a visible box (zero-size or offscreen-only) is activated programmatically.\n if (!point) {\n if (action.button && action.button !== \"left\") throw new Error(`${action.ref} has no visible box to ${action.button}-click`)\n await callOn(session, action.ref, element, PROGRAMMATIC_CLICK, [])\n } else await clickAt(session, point, action.button)\n return {}\n}\n\nasync function type(session: PageSession, action: Target & { text: string; clear: boolean }): Promise<PageActionResult> {\n const element = await resolveElement(session, action.ref, action.backendNodeId)\n await session.channel.send(\"DOM.scrollIntoViewIfNeeded\", { backendNodeId: action.backendNodeId }).catch(() => undefined)\n await session.checkpoint()\n const field = await callOn(session, action.ref, element, FIELD, [], false) as string\n const mode = await callOn(session, action.ref, field, PREPARE, [action.clear])\n if (mode === \"value\" || mode === \"append\") await callOn(session, action.ref, field, SET_VALUE, [action.text, mode === \"append\"])\n else if (action.text) await markedInput(session, dispatch => dispatch(\"Input.insertText\", { text: action.text }))\n else if (action.clear) await callOn(session, action.ref, field, DELETE_SELECTION, [])\n return { value: String(await callOn(session, action.ref, field, READ_VALUE, [])) }\n}\n\nasync function focusedElement(session: PageSession): Promise<string | undefined> {\n const active = await session.channel.send(\"Runtime.evaluate\", { expression: \"document.activeElement\", objectGroup: OBJECT_GROUP })\n await session.checkpoint()\n const objectId = (active.result as { objectId?: unknown } | undefined)?.objectId\n if (typeof objectId !== \"string\") return undefined\n const described = await callOn(session, \"the focused element\", objectId, DESCRIBE, [])\n return typeof described === \"string\" ? described : undefined\n}\n\nconst LABEL = `const label = option => (option.label || option.textContent || \"\").replace(/\\\\s+/g, \" \").trim();`\n\n/** Picks options like a user would: by visible label, else by value; change fires only on a change.\n * `:disabled` also covers disabled fieldsets and optgroups. */\nconst CHOOSE = `${LABEL}\n const apply = (select, picked) => {\n if (select.matches(\":disabled\")) throw new Error(\"codenomad:disabled\");\n for (const option of picked) if (option.matches(\":disabled\")) throw new Error(\"codenomad:option-disabled:\" + label(option));\n if (!select.multiple && picked.length !== 1) throw new Error(\"codenomad:single\");\n select.focus();\n const options = Array.from(select.options);\n if (options.some(option => option.selected !== picked.includes(option))) {\n for (const option of options) option.selected = picked.includes(option);\n select.dispatchEvent(new Event(\"input\", { bubbles: true, composed: true }));\n select.dispatchEvent(new Event(\"change\", { bubbles: true }));\n }\n return Array.from(select.selectedOptions).map(label);\n };\n const choose = (select, wanted) => {\n if (select.matches(\":disabled\")) throw new Error(\"codenomad:disabled\");\n const options = Array.from(select.options), picked = [];\n for (const raw of wanted) {\n const target = String(raw).replace(/\\\\s+/g, \" \").trim();\n const option = options.find(item => label(item) === target) || options.find(item => item.value === String(raw));\n if (!option) throw new Error(\"codenomad:no-option:\" + target);\n if (!picked.includes(option)) picked.push(option);\n }\n return apply(select, picked);\n };`\n\n/** A native select's popup is not part of the page: clicking an option picks that exact option,\n * and the select itself is changed with `select` rather than opened. */\nconst OPTION_CLICK = `${CHOOSE}\n const option = element.closest(\"option\"), select = option && option.closest(\"select\");\n if (option && select) return apply(select, [option]);\n if (element instanceof HTMLSelectElement) throw new Error(\"codenomad:is-select\");\n return null;`\n\nconst PROGRAMMATIC_CLICK = `if (element instanceof HTMLElement) element.click();\n else element.dispatchEvent(new MouseEvent(\"click\", { bubbles: true, cancelable: true, composed: true }));\n return null;`\n\nconst SELECT = `${CHOOSE}\n const control = element instanceof HTMLLabelElement ? element.control : element;\n const select = control instanceof HTMLSelectElement ? control : control && control.closest(\"select\");\n if (!select) throw new Error(\"codenomad:not-select\");\n return choose(select, args[0]);`\n\nconst FOCUS = `if (element.matches(\":disabled\")) throw new Error(\"codenomad:disabled\");\n if (element instanceof HTMLElement || element instanceof SVGElement) element.focus();\n if (element.getRootNode().activeElement !== element) throw new Error(\"codenomad:unfocusable\");\n return null;`\n\nconst DESCRIBE = `const role = element.getAttribute(\"role\") || element.tagName.toLowerCase();\n const name = (element.getAttribute(\"aria-label\") || element.getAttribute(\"title\") || element.getAttribute(\"placeholder\")\n || (element.labels && element.labels[0] && element.labels[0].textContent) || element.textContent || \"\").replace(/\\\\s+/g, \" \").trim().slice(0, 80);\n return role + (name ? \" \" + JSON.stringify(name) : \"\");`\n\nconst NOT_TEXT = [\"checkbox\", \"radio\", \"button\", \"submit\", \"reset\", \"file\", \"image\", \"hidden\"]\nconst VALUE_TYPES = [\"date\", \"time\", \"datetime-local\", \"month\", \"week\", \"range\", \"color\"]\n/** Types whose value sanitization empties an invalid value. */\nconst PROBED_TYPES = [\"date\", \"time\", \"datetime-local\", \"month\", \"week\", \"number\"]\nconst TEXT_FIELD = `const textField = item => item instanceof HTMLInputElement || item instanceof HTMLTextAreaElement\n || (item instanceof HTMLElement && item.isContentEditable);`\n\n/** The field a ref designates: itself, its labelled control, or its first editable descendant. */\nconst FIELD = `${TEXT_FIELD}\n const control = element instanceof HTMLLabelElement ? element.control : element;\n const field = control && textField(control) ? control\n : element.querySelector('input:not([type=\"hidden\"]), textarea, [contenteditable]:not([contenteditable=\"false\"])');\n if (!field || !textField(field)) throw new Error(\"codenomad:not-editable\");\n return field;`\n\n/** Native text insertion goes to the focused element, so a field that cannot take focus\n * (hidden, disabled, behind a modal dialog) fails here instead of typing into another one. */\nconst PREPARE = `const field = element, clear = args[0];\n if (field.matches(\":disabled\")) throw new Error(\"codenomad:disabled\");\n if (field.readOnly) throw new Error(\"codenomad:readonly\");\n if (field instanceof HTMLInputElement && ${JSON.stringify(NOT_TEXT)}.includes(field.type)) throw new Error(\"codenomad:not-text\");\n field.focus();\n const active = field.getRootNode().activeElement;\n const editable = !(field instanceof HTMLInputElement || field instanceof HTMLTextAreaElement);\n if (active !== field && !(editable && active instanceof HTMLElement && active.isContentEditable && active.contains(field))) {\n throw new Error(\"codenomad:unfocusable\");\n }\n if (field instanceof HTMLInputElement && ${JSON.stringify(VALUE_TYPES)}.includes(field.type)) return \"value\";\n if (editable) {\n const range = document.createRange();\n range.selectNodeContents(field);\n if (!clear) range.collapse(false);\n const selection = getSelection();\n selection.removeAllRanges();\n selection.addRange(range);\n } else if (clear) field.select();\n // Email and number inputs have no caret API: their text is appended as a value.\n else { try { field.setSelectionRange(field.value.length, field.value.length); } catch { return \"append\"; } }\n return \"insert\";`\n\n/** Sets an input's value like a picker would. An invalid value is refused before anything changes;\n * a valid one keeps the browser's normalization (lowercase colors, snapped ranges). */\nconst SET_VALUE = `const [text, append] = args, next = append ? element.value + text : text;\n // Ranges and colors have no empty value: the browser would substitute its default.\n const valid = element.type === \"color\" ? /^#[0-9a-f]{6}$/i.test(next)\n : element.type === \"range\" ? /^-?(?:\\\\d+(?:\\\\.\\\\d+)?|\\\\.\\\\d+)(?:[eE][+-]?\\\\d+)?$/.test(next)\n : next !== \"\" && ${JSON.stringify(PROBED_TYPES)}.includes(element.type) ? (() => { const probe = document.createElement(\"input\"); probe.type = element.type; probe.value = next; return probe.value !== \"\"; })()\n : true;\n if (!valid) throw new Error(\"codenomad:invalid-value:\" + next);\n if (next === element.value) return null;\n Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, \"value\").set.call(element, next);\n element.dispatchEvent(new Event(\"input\", { bubbles: true, composed: true }));\n element.dispatchEvent(new Event(\"change\", { bubbles: true }));\n return null;`\n\nconst DELETE_SELECTION = `document.execCommand(\"delete\"); return null;`\n\nconst READ_VALUE = `const value = element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement ? element.value : element.innerText || element.textContent || \"\";\n if (element instanceof HTMLInputElement && element.type === \"password\") return \"•\".repeat(Math.min(value.length, 16));\n return value.length > ${VALUE_LIMIT} ? value.slice(0, ${VALUE_LIMIT - 1}) + \"…\" : value;`\n"]}