@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,130 @@
1
+ import { keyChord } from "./keys";
2
+ import { callOn, Handle, markedInput, OBJECT_GROUP, resolveElement } from "./protocol";
3
+ /** The point a user would click, or undefined for an element without a visible box. */
4
+ export async function aim(session, ref, backendNodeId, options = {}) {
5
+ const { channel } = session;
6
+ if (options.scroll !== false)
7
+ await channel.send("DOM.scrollIntoViewIfNeeded", { backendNodeId }).catch(() => undefined);
8
+ await session.checkpoint();
9
+ const result = await channel.send("DOM.getContentQuads", { backendNodeId }).catch(() => ({}));
10
+ await session.checkpoint();
11
+ const quads = Array.isArray(result.quads) ? result.quads : [];
12
+ const viewport = await layoutViewport(session);
13
+ const quad = quads.find(item => item.length === 8 && area(item) >= 1);
14
+ if (!quad)
15
+ return undefined;
16
+ // The visible part of the box, so a half-clipped element is aimed where it shows.
17
+ const xs = [quad[0], quad[2], quad[4], quad[6]], ys = [quad[1], quad[3], quad[5], quad[7]];
18
+ const left = Math.max(0, Math.min(...xs)), right = Math.min(viewport.width, Math.max(...xs));
19
+ const top = Math.max(0, Math.min(...ys)), bottom = Math.min(viewport.height, Math.max(...ys));
20
+ if (right - left < 1 || bottom - top < 1)
21
+ return undefined;
22
+ const point = { x: (left + right) / 2, y: (top + bottom) / 2 };
23
+ if (options.hitTest !== false)
24
+ await assertReceives(session, ref, backendNodeId, point);
25
+ return point;
26
+ }
27
+ /** Refuses a point that another element covers, instead of acting on whatever is on top. */
28
+ async function assertReceives(session, ref, backendNodeId, point) {
29
+ // Like real events, hit testing passes through `pointer-events: none` layers (toasts, overlays' gaps).
30
+ const hit = await session.channel.send("DOM.getNodeForLocation", { x: Math.floor(point.x), y: Math.floor(point.y),
31
+ includeUserAgentShadowDOM: false, ignorePointerEventsNone: false }).catch(() => undefined);
32
+ await session.checkpoint();
33
+ const hitNode = hit?.backendNodeId;
34
+ if (typeof hitNode !== "number" || hitNode === backendNodeId)
35
+ return;
36
+ const target = await resolveElement(session, ref, backendNodeId);
37
+ const other = await resolveElement(session, ref, hitNode).catch(() => undefined);
38
+ if (!other)
39
+ return;
40
+ const covered = await callOn(session, ref, target, RECEIVES, [new Handle(other)]);
41
+ if (typeof covered === "string")
42
+ throw new Error(`${ref} is covered by ${covered}; close or move it first, then inspect again`);
43
+ }
44
+ const RECEIVES = `const hit = args[0] && (args[0].nodeType === Node.ELEMENT_NODE ? args[0] : args[0].parentElement);
45
+ if (!hit || element === hit || element.contains(hit)) return null;
46
+ const label = hit.closest("label");
47
+ if (label && (label.control === element || label.contains(element))) return null;
48
+ const name = (hit.getAttribute("aria-label") || hit.getAttribute("title") || hit.innerText || "").replace(/\\s+/g, " ").trim().slice(0, 60);
49
+ return hit.tagName.toLowerCase() + (name ? " " + JSON.stringify(name) : "");`;
50
+ async function layoutViewport(session) {
51
+ const metrics = await session.channel.send("Page.getLayoutMetrics").catch(() => ({}));
52
+ const viewport = (metrics.cssLayoutViewport ?? metrics.layoutViewport);
53
+ return { width: viewport?.clientWidth ?? Number.POSITIVE_INFINITY, height: viewport?.clientHeight ?? Number.POSITIVE_INFINITY };
54
+ }
55
+ function area(quad) {
56
+ let sum = 0;
57
+ for (let index = 0; index < 4; index++) {
58
+ const next = (index + 1) % 4;
59
+ sum += quad[index * 2] * quad[next * 2 + 1] - quad[next * 2] * quad[index * 2 + 1];
60
+ }
61
+ return Math.abs(sum) / 2;
62
+ }
63
+ const BUTTONS = { left: 1, right: 2, middle: 4 };
64
+ export async function clickAt(session, point, button = "left") {
65
+ await markedInput(session, async (dispatch) => {
66
+ await dispatch("Input.dispatchMouseEvent", { type: "mouseMoved", ...point });
67
+ await dispatch("Input.dispatchMouseEvent", { type: "mousePressed", ...point, button, buttons: BUTTONS[button], clickCount: 1 });
68
+ await dispatch("Input.dispatchMouseEvent", { type: "mouseReleased", ...point, button, buttons: 0, clickCount: 1 });
69
+ });
70
+ }
71
+ export async function hoverAt(session, point) {
72
+ await markedInput(session, dispatch => dispatch("Input.dispatchMouseEvent", { type: "mouseMoved", ...point }));
73
+ }
74
+ export async function wheelAt(session, point, delta) {
75
+ const at = point ?? await viewportCentre(session);
76
+ await markedInput(session, async (dispatch) => {
77
+ await dispatch("Input.dispatchMouseEvent", { type: "mouseMoved", ...at });
78
+ await dispatch("Input.dispatchMouseEvent", { type: "mouseWheel", ...at, deltaX: delta.x, deltaY: delta.y });
79
+ });
80
+ }
81
+ /** A pointer drag in small steps, so pointer-based drag libraries pass their activation distance. */
82
+ export async function dragBetween(session, from, to) {
83
+ await markedInput(session, async (dispatch) => {
84
+ await dispatch("Input.dispatchMouseEvent", { type: "mouseMoved", ...from });
85
+ await dispatch("Input.dispatchMouseEvent", { type: "mousePressed", ...from, button: "left", buttons: 1, clickCount: 1 });
86
+ const steps = 10;
87
+ for (let step = 1; step <= steps; step++) {
88
+ await dispatch("Input.dispatchMouseEvent", { type: "mouseMoved", x: from.x + (to.x - from.x) * step / steps,
89
+ y: from.y + (to.y - from.y) * step / steps, button: "left", buttons: 1 });
90
+ }
91
+ await dispatch("Input.dispatchMouseEvent", { type: "mouseReleased", ...to, button: "left", buttons: 0, clickCount: 1 });
92
+ });
93
+ }
94
+ export async function pressKeys(session, chord) {
95
+ const events = keyChord(chord);
96
+ await markedInput(session, async (dispatch) => {
97
+ for (const event of events)
98
+ await dispatch("Input.dispatchKeyEvent", event);
99
+ });
100
+ }
101
+ async function viewportCentre(session) {
102
+ const { width, height } = await layoutViewport(session);
103
+ return { x: Number.isFinite(width) ? width / 2 : 400, y: Number.isFinite(height) ? height / 2 : 300 };
104
+ }
105
+ /** Polls the page for text, in its rendered text or in labels, titles, placeholders and values. */
106
+ export async function waitForText(session, text, gone, timeoutMs) {
107
+ const started = Date.now();
108
+ for (;;) {
109
+ const root = await session.channel.send("Runtime.evaluate", { expression: "document.documentElement", objectGroup: OBJECT_GROUP });
110
+ await session.checkpoint();
111
+ const objectId = root.result?.objectId;
112
+ const found = typeof objectId === "string" && await callOn(session, "the page", objectId, CONTAINS, [text]) === true;
113
+ const waitedMs = Date.now() - started;
114
+ if (found !== gone)
115
+ return { found, waitedMs };
116
+ if (waitedMs >= timeoutMs) {
117
+ throw new Error(gone ? `${JSON.stringify(text)} is still shown after ${timeoutMs} ms` : `${JSON.stringify(text)} did not appear within ${timeoutMs} ms`);
118
+ }
119
+ await new Promise(resolve => setTimeout(resolve, Math.min(200, timeoutMs - waitedMs)));
120
+ }
121
+ }
122
+ const CONTAINS = `const needle = String(args[0]).replace(/\\s+/g, " ").trim().toLowerCase();
123
+ const root = element;
124
+ const text = value => String(value || "").replace(/\\s+/g, " ").toLowerCase();
125
+ if (text(root.innerText || root.textContent).includes(needle)) return true;
126
+ for (const item of root.querySelectorAll("[aria-label], [title], [placeholder], input, textarea")) {
127
+ if ([item.getAttribute("aria-label"), item.getAttribute("title"), item.getAttribute("placeholder"), item.value].some(value => text(value).includes(needle))) return true;
128
+ }
129
+ return false;`;
130
+ //# sourceMappingURL=input.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"input.js","sourceRoot":"","sources":["../../../src/automation/page/input.ts"],"names":[],"mappings":"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","sourcesContent":["import { keyChord } from \"./keys\"\nimport { callOn, Handle, markedInput, OBJECT_GROUP, resolveElement, type PageSession } from \"./protocol\"\n\n/** Trusted pointer, wheel and keyboard input through DevTools, aimed like a user would:\n * at the element's visible centre, only when that point actually reaches the element. */\n\nexport interface Point { x: number; y: number }\n\n/** The point a user would click, or undefined for an element without a visible box. */\nexport async function aim(session: PageSession, ref: string, backendNodeId: number, options: { hitTest?: boolean; scroll?: boolean } = {}): Promise<Point | undefined> {\n const { channel } = session\n if (options.scroll !== false) await channel.send(\"DOM.scrollIntoViewIfNeeded\", { backendNodeId }).catch(() => undefined)\n await session.checkpoint()\n const result = await channel.send(\"DOM.getContentQuads\", { backendNodeId }).catch(() => ({} as Record<string, unknown>))\n await session.checkpoint()\n const quads = Array.isArray(result.quads) ? result.quads as number[][] : []\n const viewport = await layoutViewport(session)\n const quad = quads.find(item => item.length === 8 && area(item) >= 1)\n if (!quad) return undefined\n // The visible part of the box, so a half-clipped element is aimed where it shows.\n const xs = [quad[0], quad[2], quad[4], quad[6]], ys = [quad[1], quad[3], quad[5], quad[7]]\n const left = Math.max(0, Math.min(...xs)), right = Math.min(viewport.width, Math.max(...xs))\n const top = Math.max(0, Math.min(...ys)), bottom = Math.min(viewport.height, Math.max(...ys))\n if (right - left < 1 || bottom - top < 1) return undefined\n const point = { x: (left + right) / 2, y: (top + bottom) / 2 }\n if (options.hitTest !== false) await assertReceives(session, ref, backendNodeId, point)\n return point\n}\n\n/** Refuses a point that another element covers, instead of acting on whatever is on top. */\nasync function assertReceives(session: PageSession, ref: string, backendNodeId: number, point: Point): Promise<void> {\n // Like real events, hit testing passes through `pointer-events: none` layers (toasts, overlays' gaps).\n const hit = await session.channel.send(\"DOM.getNodeForLocation\", { x: Math.floor(point.x), y: Math.floor(point.y),\n includeUserAgentShadowDOM: false, ignorePointerEventsNone: false }).catch(() => undefined)\n await session.checkpoint()\n const hitNode = hit?.backendNodeId\n if (typeof hitNode !== \"number\" || hitNode === backendNodeId) return\n const target = await resolveElement(session, ref, backendNodeId)\n const other = await resolveElement(session, ref, hitNode).catch(() => undefined)\n if (!other) return\n const covered = await callOn(session, ref, target, RECEIVES, [new Handle(other)])\n if (typeof covered === \"string\") throw new Error(`${ref} is covered by ${covered}; close or move it first, then inspect again`)\n}\n\nconst RECEIVES = `const hit = args[0] && (args[0].nodeType === Node.ELEMENT_NODE ? args[0] : args[0].parentElement);\n if (!hit || element === hit || element.contains(hit)) return null;\n const label = hit.closest(\"label\");\n if (label && (label.control === element || label.contains(element))) return null;\n const name = (hit.getAttribute(\"aria-label\") || hit.getAttribute(\"title\") || hit.innerText || \"\").replace(/\\\\s+/g, \" \").trim().slice(0, 60);\n return hit.tagName.toLowerCase() + (name ? \" \" + JSON.stringify(name) : \"\");`\n\nasync function layoutViewport(session: PageSession): Promise<{ width: number; height: number }> {\n const metrics = await session.channel.send(\"Page.getLayoutMetrics\").catch(() => ({} as Record<string, unknown>))\n const viewport = (metrics.cssLayoutViewport ?? metrics.layoutViewport) as { clientWidth?: number; clientHeight?: number } | undefined\n return { width: viewport?.clientWidth ?? Number.POSITIVE_INFINITY, height: viewport?.clientHeight ?? Number.POSITIVE_INFINITY }\n}\n\nfunction area(quad: number[]): number {\n let sum = 0\n for (let index = 0; index < 4; index++) {\n const next = (index + 1) % 4\n sum += quad[index * 2] * quad[next * 2 + 1] - quad[next * 2] * quad[index * 2 + 1]\n }\n return Math.abs(sum) / 2\n}\n\nconst BUTTONS = { left: 1, right: 2, middle: 4 } as const\nexport type MouseButton = keyof typeof BUTTONS\n\nexport async function clickAt(session: PageSession, point: Point, button: MouseButton = \"left\"): Promise<void> {\n await markedInput(session, async dispatch => {\n await dispatch(\"Input.dispatchMouseEvent\", { type: \"mouseMoved\", ...point })\n await dispatch(\"Input.dispatchMouseEvent\", { type: \"mousePressed\", ...point, button, buttons: BUTTONS[button], clickCount: 1 })\n await dispatch(\"Input.dispatchMouseEvent\", { type: \"mouseReleased\", ...point, button, buttons: 0, clickCount: 1 })\n })\n}\n\nexport async function hoverAt(session: PageSession, point: Point): Promise<void> {\n await markedInput(session, dispatch => dispatch(\"Input.dispatchMouseEvent\", { type: \"mouseMoved\", ...point }))\n}\n\nexport async function wheelAt(session: PageSession, point: Point | undefined, delta: { x: number; y: number }): Promise<void> {\n const at = point ?? await viewportCentre(session)\n await markedInput(session, async dispatch => {\n await dispatch(\"Input.dispatchMouseEvent\", { type: \"mouseMoved\", ...at })\n await dispatch(\"Input.dispatchMouseEvent\", { type: \"mouseWheel\", ...at, deltaX: delta.x, deltaY: delta.y })\n })\n}\n\n/** A pointer drag in small steps, so pointer-based drag libraries pass their activation distance. */\nexport async function dragBetween(session: PageSession, from: Point, to: Point): Promise<void> {\n await markedInput(session, async dispatch => {\n await dispatch(\"Input.dispatchMouseEvent\", { type: \"mouseMoved\", ...from })\n await dispatch(\"Input.dispatchMouseEvent\", { type: \"mousePressed\", ...from, button: \"left\", buttons: 1, clickCount: 1 })\n const steps = 10\n for (let step = 1; step <= steps; step++) {\n await dispatch(\"Input.dispatchMouseEvent\", { type: \"mouseMoved\", x: from.x + (to.x - from.x) * step / steps,\n y: from.y + (to.y - from.y) * step / steps, button: \"left\", buttons: 1 })\n }\n await dispatch(\"Input.dispatchMouseEvent\", { type: \"mouseReleased\", ...to, button: \"left\", buttons: 0, clickCount: 1 })\n })\n}\n\nexport async function pressKeys(session: PageSession, chord: string): Promise<void> {\n const events = keyChord(chord)\n await markedInput(session, async dispatch => {\n for (const event of events) await dispatch(\"Input.dispatchKeyEvent\", event)\n })\n}\n\nasync function viewportCentre(session: PageSession): Promise<Point> {\n const { width, height } = await layoutViewport(session)\n return { x: Number.isFinite(width) ? width / 2 : 400, y: Number.isFinite(height) ? height / 2 : 300 }\n}\n\n/** Polls the page for text, in its rendered text or in labels, titles, placeholders and values. */\nexport async function waitForText(session: PageSession, text: string, gone: boolean, timeoutMs: number): Promise<{ found: boolean; waitedMs: number }> {\n const started = Date.now()\n for (;;) {\n const root = await session.channel.send(\"Runtime.evaluate\", { expression: \"document.documentElement\", objectGroup: OBJECT_GROUP })\n await session.checkpoint()\n const objectId = (root.result as { objectId?: unknown } | undefined)?.objectId\n const found = typeof objectId === \"string\" && await callOn(session, \"the page\", objectId, CONTAINS, [text]) === true\n const waitedMs = Date.now() - started\n if (found !== gone) return { found, waitedMs }\n if (waitedMs >= timeoutMs) {\n throw new Error(gone ? `${JSON.stringify(text)} is still shown after ${timeoutMs} ms` : `${JSON.stringify(text)} did not appear within ${timeoutMs} ms`)\n }\n await new Promise(resolve => setTimeout(resolve, Math.min(200, timeoutMs - waitedMs)))\n }\n}\n\nconst CONTAINS = `const needle = String(args[0]).replace(/\\\\s+/g, \" \").trim().toLowerCase();\n const root = element;\n const text = value => String(value || \"\").replace(/\\\\s+/g, \" \").toLowerCase();\n if (text(root.innerText || root.textContent).includes(needle)) return true;\n for (const item of root.querySelectorAll(\"[aria-label], [title], [placeholder], input, textarea\")) {\n if ([item.getAttribute(\"aria-label\"), item.getAttribute(\"title\"), item.getAttribute(\"placeholder\"), item.value].some(value => text(value).includes(needle))) return true;\n }\n return false;`\n"]}
@@ -0,0 +1,71 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+ import { clickAt, dragBetween, pressKeys } from "./input";
4
+ test("failed pointer and key sequences release potentially held inputs without replaying the action", async () => {
5
+ for (const kind of ["click", "drag", "keys"]) {
6
+ const calls = [];
7
+ let pressed = false, failed = false;
8
+ const session = {
9
+ markAutomation: true,
10
+ channel: { send: async (method, params = {}) => {
11
+ calls.push({ method, params });
12
+ if (params.type === "mousePressed" || params.type === "rawKeyDown")
13
+ pressed = true;
14
+ return method === "Runtime.evaluate" ? { result: { value: 1_700_000_000 } } : {};
15
+ } },
16
+ checkpoint: async () => { if (pressed && !failed) {
17
+ failed = true;
18
+ throw new Error("Page changed");
19
+ } },
20
+ };
21
+ await assert.rejects(kind === "click" ? clickAt(session, { x: 10, y: 20 })
22
+ : kind === "drag" ? dragBetween(session, { x: 10, y: 20 }, { x: 50, y: 60 }) : pressKeys(session, "Control+K"), /Page changed/);
23
+ const inputs = calls.filter(call => call.method.startsWith("Input."));
24
+ assert.equal(inputs.filter(call => call.params.type === "mousePressed" || call.params.type === "rawKeyDown").length, 1, "the action is admitted only once");
25
+ assert.equal(inputs.at(-1)?.params.type, kind === "keys" ? "keyUp" : "mouseReleased");
26
+ assert.equal(inputs.at(-1)?.params[kind === "keys" ? "key" : "button"], kind === "keys" ? "Control" : "left");
27
+ assert.match(String(calls.at(-1)?.params.expression), /Math.min/, "the retained input interval ends only after release");
28
+ }
29
+ });
30
+ test("a frozen mark write cannot suppress release, and late input effects are reported as unknown", async () => {
31
+ for (const recoversBeforeRelease of [true, false]) {
32
+ const calls = [];
33
+ let frozen = false, clicks = 0, queuedRelease;
34
+ const session = {
35
+ markAutomation: true,
36
+ checkpoint: async () => { },
37
+ channel: { send: async (method, params = {}) => {
38
+ calls.push({ method, params });
39
+ if (method === "Runtime.evaluate" && frozen)
40
+ throw new Error("CDP mark timed out");
41
+ if (params.type === "mousePressed") {
42
+ frozen = true;
43
+ throw new Error("CDP input timed out");
44
+ }
45
+ if (params.type === "mouseReleased") {
46
+ if (recoversBeforeRelease)
47
+ clicks++;
48
+ else {
49
+ queuedRelease = () => { clicks++; };
50
+ throw new Error("CDP release timed out");
51
+ }
52
+ }
53
+ return method === "Runtime.evaluate" ? { result: { value: 1_700_000_000 } } : {};
54
+ } },
55
+ };
56
+ await assert.rejects(clickAt(session, { x: 10, y: 20 }), /outcome is unknown.*may still be delivered; inspect before retrying/);
57
+ assert.equal(calls.filter(call => call.params.type === "mousePressed").length, 1);
58
+ assert.equal(calls.filter(call => call.params.type === "mouseReleased").length, 1, "release is attempted even if its mark write times out");
59
+ assert.equal(calls.find(call => call.params.type === "mouseReleased")?.params.timestamp, calls.find(call => call.params.type === "mousePressed")?.params.timestamp, "late release retains the original press timestamp");
60
+ queuedRelease?.();
61
+ assert.equal(clicks, 1, "cleanup can complete the admitted click after a timeout, but never replays its press");
62
+ }
63
+ });
64
+ test("input refuses when the page cannot supply a valid provenance timestamp", async () => {
65
+ const methods = [];
66
+ await assert.rejects(pressKeys({ markAutomation: true, checkpoint: async () => { },
67
+ channel: { send: async (method) => { methods.push(method); return { result: { value: null } }; } },
68
+ }, "Enter"), /Cannot establish automation input provenance/);
69
+ assert.ok(methods.every(method => method === "Runtime.evaluate"));
70
+ });
71
+ //# sourceMappingURL=input.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"input.test.js","sourceRoot":"","sources":["../../../src/automation/page/input.test.ts"],"names":[],"mappings":"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","sourcesContent":["import assert from \"node:assert/strict\"\nimport { test } from \"node:test\"\nimport { clickAt, dragBetween, pressKeys } from \"./input\"\nimport type { PageSession } from \"./protocol\"\n\ntest(\"failed pointer and key sequences release potentially held inputs without replaying the action\", async () => {\n for (const kind of [\"click\", \"drag\", \"keys\"] as const) {\n const calls: Array<{ method: string; params: Record<string, unknown> }> = []\n let pressed = false, failed = false\n const session: PageSession = {\n markAutomation: true,\n channel: { send: async (method, params = {}) => {\n calls.push({ method, params })\n if (params.type === \"mousePressed\" || params.type === \"rawKeyDown\") pressed = true\n return method === \"Runtime.evaluate\" ? { result: { value: 1_700_000_000 } } : {}\n } },\n checkpoint: async () => { if (pressed && !failed) { failed = true; throw new Error(\"Page changed\") } },\n }\n await assert.rejects(kind === \"click\" ? clickAt(session, { x: 10, y: 20 })\n : kind === \"drag\" ? dragBetween(session, { x: 10, y: 20 }, { x: 50, y: 60 }) : pressKeys(session, \"Control+K\"), /Page changed/)\n const inputs = calls.filter(call => call.method.startsWith(\"Input.\"))\n assert.equal(inputs.filter(call => call.params.type === \"mousePressed\" || call.params.type === \"rawKeyDown\").length, 1,\n \"the action is admitted only once\")\n assert.equal(inputs.at(-1)?.params.type, kind === \"keys\" ? \"keyUp\" : \"mouseReleased\")\n assert.equal(inputs.at(-1)?.params[kind === \"keys\" ? \"key\" : \"button\"], kind === \"keys\" ? \"Control\" : \"left\")\n assert.match(String(calls.at(-1)?.params.expression), /Math.min/, \"the retained input interval ends only after release\")\n }\n})\n\ntest(\"a frozen mark write cannot suppress release, and late input effects are reported as unknown\", async () => {\n for (const recoversBeforeRelease of [true, false]) {\n const calls: Array<{ method: string; params: Record<string, unknown> }> = []\n let frozen = false, clicks = 0, queuedRelease: (() => void) | undefined\n const session: PageSession = {\n markAutomation: true,\n checkpoint: async () => {},\n channel: { send: async (method, params = {}) => {\n calls.push({ method, params })\n if (method === \"Runtime.evaluate\" && frozen) throw new Error(\"CDP mark timed out\")\n if (params.type === \"mousePressed\") { frozen = true; throw new Error(\"CDP input timed out\") }\n if (params.type === \"mouseReleased\") {\n if (recoversBeforeRelease) clicks++\n else { queuedRelease = () => { clicks++ }; throw new Error(\"CDP release timed out\") }\n }\n return method === \"Runtime.evaluate\" ? { result: { value: 1_700_000_000 } } : {}\n } },\n }\n await assert.rejects(clickAt(session, { x: 10, y: 20 }), /outcome is unknown.*may still be delivered; inspect before retrying/)\n assert.equal(calls.filter(call => call.params.type === \"mousePressed\").length, 1)\n assert.equal(calls.filter(call => call.params.type === \"mouseReleased\").length, 1,\n \"release is attempted even if its mark write times out\")\n assert.equal(calls.find(call => call.params.type === \"mouseReleased\")?.params.timestamp,\n calls.find(call => call.params.type === \"mousePressed\")?.params.timestamp, \"late release retains the original press timestamp\")\n queuedRelease?.()\n assert.equal(clicks, 1, \"cleanup can complete the admitted click after a timeout, but never replays its press\")\n }\n})\n\ntest(\"input refuses when the page cannot supply a valid provenance timestamp\", async () => {\n const methods: string[] = []\n await assert.rejects(pressKeys({ markAutomation: true, checkpoint: async () => {},\n channel: { send: async method => { methods.push(method); return { result: { value: null } } } },\n }, \"Enter\"), /Cannot establish automation input provenance/)\n assert.ok(methods.every(method => method === \"Runtime.evaluate\"))\n})\n"]}
@@ -0,0 +1,99 @@
1
+ /** Keyboard chords for `press`, as DevTools `Input.dispatchKeyEvent` sequences. */
2
+ const MODIFIERS = {
3
+ Alt: { definition: { key: "Alt", code: "AltLeft", keyCode: 18, location: 1 }, bit: 1 },
4
+ Control: { definition: { key: "Control", code: "ControlLeft", keyCode: 17, location: 1 }, bit: 2 },
5
+ Meta: { definition: { key: "Meta", code: "MetaLeft", keyCode: 91, location: 1 }, bit: 4 },
6
+ Shift: { definition: { key: "Shift", code: "ShiftLeft", keyCode: 16, location: 1 }, bit: 8 },
7
+ };
8
+ const MODIFIER_ALIASES = { alt: "Alt", option: "Alt", control: "Control", ctrl: "Control",
9
+ meta: "Meta", cmd: "Meta", command: "Meta", shift: "Shift" };
10
+ const NAMED = {
11
+ Enter: { key: "Enter", code: "Enter", keyCode: 13, text: "\r" },
12
+ Tab: { key: "Tab", code: "Tab", keyCode: 9 },
13
+ Escape: { key: "Escape", code: "Escape", keyCode: 27 },
14
+ Backspace: { key: "Backspace", code: "Backspace", keyCode: 8 },
15
+ Delete: { key: "Delete", code: "Delete", keyCode: 46 },
16
+ Space: { key: " ", code: "Space", keyCode: 32, text: " " },
17
+ ArrowUp: { key: "ArrowUp", code: "ArrowUp", keyCode: 38 },
18
+ ArrowDown: { key: "ArrowDown", code: "ArrowDown", keyCode: 40 },
19
+ ArrowLeft: { key: "ArrowLeft", code: "ArrowLeft", keyCode: 37 },
20
+ ArrowRight: { key: "ArrowRight", code: "ArrowRight", keyCode: 39 },
21
+ Home: { key: "Home", code: "Home", keyCode: 36 },
22
+ End: { key: "End", code: "End", keyCode: 35 },
23
+ PageUp: { key: "PageUp", code: "PageUp", keyCode: 33 },
24
+ PageDown: { key: "PageDown", code: "PageDown", keyCode: 34 },
25
+ Insert: { key: "Insert", code: "Insert", keyCode: 45 },
26
+ ContextMenu: { key: "ContextMenu", code: "ContextMenu", keyCode: 93 },
27
+ ...Object.fromEntries(Array.from({ length: 12 }, (_, index) => [`F${index + 1}`, { key: `F${index + 1}`, code: `F${index + 1}`, keyCode: 112 + index }])),
28
+ };
29
+ const NAMED_ALIASES = { esc: "Escape", return: "Enter", del: "Delete", up: "ArrowUp", down: "ArrowDown",
30
+ left: "ArrowLeft", right: "ArrowRight", pgup: "PageUp", pgdn: "PageDown", " ": "Space" };
31
+ /** US-layout punctuation: [code, key code, shifted character]. */
32
+ const PUNCTUATION = {
33
+ "`": ["Backquote", 192, "~"], "-": ["Minus", 189, "_"], "=": ["Equal", 187, "+"], "[": ["BracketLeft", 219, "{"],
34
+ "]": ["BracketRight", 221, "}"], "\\": ["Backslash", 220, "|"], ";": ["Semicolon", 186, ":"], "'": ["Quote", 222, "\""],
35
+ ",": ["Comma", 188, "<"], ".": ["Period", 190, ">"], "/": ["Slash", 191, "?"],
36
+ };
37
+ const SHIFTED_DIGITS = ")!@#$%^&*(";
38
+ function character(value) {
39
+ if (value.length !== 1)
40
+ return undefined;
41
+ if (/[a-z]/i.test(value))
42
+ return { key: value, code: `Key${value.toUpperCase()}`, keyCode: value.toUpperCase().charCodeAt(0), text: value };
43
+ if (/[0-9]/.test(value))
44
+ return { key: value, code: `Digit${value}`, keyCode: value.charCodeAt(0), text: value };
45
+ const shiftedDigit = SHIFTED_DIGITS.indexOf(value);
46
+ if (shiftedDigit >= 0)
47
+ return { key: value, code: `Digit${shiftedDigit}`, keyCode: 48 + shiftedDigit, text: value };
48
+ for (const [base, [code, keyCode, shifted]] of Object.entries(PUNCTUATION)) {
49
+ if (value === base || value === shifted)
50
+ return { key: value, code, keyCode, text: value };
51
+ }
52
+ return undefined;
53
+ }
54
+ /** Parses "Enter", "a", "Shift+Tab", "Control+Shift+K" or "Mod+K" (Meta on macOS, Control elsewhere). */
55
+ export function keyChord(chord, platform = process.platform) {
56
+ const parts = chord === "+" ? ["+"] : chord.split("+").map(part => part.trim());
57
+ if (!parts.length || parts.some(part => !part) || chord.length > 64)
58
+ throw new Error(`Unknown key ${JSON.stringify(chord)}`);
59
+ const main = parts.pop();
60
+ const modifiers = [];
61
+ for (const part of parts) {
62
+ const name = part.toLowerCase() === "mod" ? (platform === "darwin" ? "Meta" : "Control") : MODIFIER_ALIASES[part.toLowerCase()];
63
+ if (!name)
64
+ throw new Error(`Unknown modifier ${JSON.stringify(part)} in ${JSON.stringify(chord)}; use Control, Shift, Alt, Meta or Mod`);
65
+ if (!modifiers.includes(name))
66
+ modifiers.push(name);
67
+ }
68
+ const named = NAMED[main] ?? NAMED[NAMED_ALIASES[main.toLowerCase()] ?? ""]
69
+ ?? Object.values(NAMED).find(definition => definition.key.toLowerCase() === main.toLowerCase() && definition.key.length > 1);
70
+ let definition = named ?? character(main);
71
+ if (!definition)
72
+ throw new Error(`Unknown key ${JSON.stringify(main)}; use a character or a key name such as Enter, Escape, Tab or ArrowDown`);
73
+ // Shift+a types "A", like a physical keyboard.
74
+ if (modifiers.includes("Shift") && definition.text && /^[a-z]$/.test(definition.text)) {
75
+ definition = { ...definition, key: definition.text.toUpperCase(), text: definition.text.toUpperCase() };
76
+ }
77
+ const events = [];
78
+ let bits = 0;
79
+ for (const name of modifiers) {
80
+ const { definition: modifier, bit } = MODIFIERS[name];
81
+ bits |= bit;
82
+ events.push(keyEvent("rawKeyDown", modifier, bits));
83
+ }
84
+ // A shortcut types nothing; a plain or shifted press produces its text.
85
+ const text = bits & ~MODIFIERS.Shift.bit ? undefined : definition.text;
86
+ events.push(keyEvent(text ? "keyDown" : "rawKeyDown", definition, bits, text));
87
+ events.push(keyEvent("keyUp", definition, bits));
88
+ for (const name of [...modifiers].reverse()) {
89
+ const { definition: modifier, bit } = MODIFIERS[name];
90
+ bits &= ~bit;
91
+ events.push(keyEvent("keyUp", modifier, bits));
92
+ }
93
+ return events;
94
+ }
95
+ function keyEvent(type, definition, modifiers, text) {
96
+ return { type, key: definition.key, code: definition.code, windowsVirtualKeyCode: definition.keyCode, modifiers,
97
+ ...(text ? { text, unmodifiedText: text } : {}), ...(definition.location ? { location: definition.location } : {}) };
98
+ }
99
+ //# sourceMappingURL=keys.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keys.js","sourceRoot":"","sources":["../../../src/automation/page/keys.ts"],"names":[],"mappings":"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","sourcesContent":["/** Keyboard chords for `press`, as DevTools `Input.dispatchKeyEvent` sequences. */\n\ninterface KeyDefinition {\n key: string\n code: string\n keyCode: number\n /** Text a plain press inserts (Enter submits forms and breaks lines). */\n text?: string\n location?: number\n}\n\nexport interface KeyEvent extends Record<string, unknown> {\n type: \"keyDown\" | \"rawKeyDown\" | \"keyUp\"\n key: string\n code: string\n windowsVirtualKeyCode: number\n modifiers: number\n text?: string\n unmodifiedText?: string\n location?: number\n}\n\nconst MODIFIERS: Record<string, { definition: KeyDefinition; bit: number }> = {\n Alt: { definition: { key: \"Alt\", code: \"AltLeft\", keyCode: 18, location: 1 }, bit: 1 },\n Control: { definition: { key: \"Control\", code: \"ControlLeft\", keyCode: 17, location: 1 }, bit: 2 },\n Meta: { definition: { key: \"Meta\", code: \"MetaLeft\", keyCode: 91, location: 1 }, bit: 4 },\n Shift: { definition: { key: \"Shift\", code: \"ShiftLeft\", keyCode: 16, location: 1 }, bit: 8 },\n}\nconst MODIFIER_ALIASES: Record<string, string> = { alt: \"Alt\", option: \"Alt\", control: \"Control\", ctrl: \"Control\",\n meta: \"Meta\", cmd: \"Meta\", command: \"Meta\", shift: \"Shift\" }\n\nconst NAMED: Record<string, KeyDefinition> = {\n Enter: { key: \"Enter\", code: \"Enter\", keyCode: 13, text: \"\\r\" },\n Tab: { key: \"Tab\", code: \"Tab\", keyCode: 9 },\n Escape: { key: \"Escape\", code: \"Escape\", keyCode: 27 },\n Backspace: { key: \"Backspace\", code: \"Backspace\", keyCode: 8 },\n Delete: { key: \"Delete\", code: \"Delete\", keyCode: 46 },\n Space: { key: \" \", code: \"Space\", keyCode: 32, text: \" \" },\n ArrowUp: { key: \"ArrowUp\", code: \"ArrowUp\", keyCode: 38 },\n ArrowDown: { key: \"ArrowDown\", code: \"ArrowDown\", keyCode: 40 },\n ArrowLeft: { key: \"ArrowLeft\", code: \"ArrowLeft\", keyCode: 37 },\n ArrowRight: { key: \"ArrowRight\", code: \"ArrowRight\", keyCode: 39 },\n Home: { key: \"Home\", code: \"Home\", keyCode: 36 },\n End: { key: \"End\", code: \"End\", keyCode: 35 },\n PageUp: { key: \"PageUp\", code: \"PageUp\", keyCode: 33 },\n PageDown: { key: \"PageDown\", code: \"PageDown\", keyCode: 34 },\n Insert: { key: \"Insert\", code: \"Insert\", keyCode: 45 },\n ContextMenu: { key: \"ContextMenu\", code: \"ContextMenu\", keyCode: 93 },\n ...Object.fromEntries(Array.from({ length: 12 }, (_, index) => [`F${index + 1}`, { key: `F${index + 1}`, code: `F${index + 1}`, keyCode: 112 + index }])),\n}\nconst NAMED_ALIASES: Record<string, string> = { esc: \"Escape\", return: \"Enter\", del: \"Delete\", up: \"ArrowUp\", down: \"ArrowDown\",\n left: \"ArrowLeft\", right: \"ArrowRight\", pgup: \"PageUp\", pgdn: \"PageDown\", \" \": \"Space\" }\n\n/** US-layout punctuation: [code, key code, shifted character]. */\nconst PUNCTUATION: Record<string, [string, number, string?]> = {\n \"`\": [\"Backquote\", 192, \"~\"], \"-\": [\"Minus\", 189, \"_\"], \"=\": [\"Equal\", 187, \"+\"], \"[\": [\"BracketLeft\", 219, \"{\"],\n \"]\": [\"BracketRight\", 221, \"}\"], \"\\\\\": [\"Backslash\", 220, \"|\"], \";\": [\"Semicolon\", 186, \":\"], \"'\": [\"Quote\", 222, \"\\\"\"],\n \",\": [\"Comma\", 188, \"<\"], \".\": [\"Period\", 190, \">\"], \"/\": [\"Slash\", 191, \"?\"],\n}\nconst SHIFTED_DIGITS = \")!@#$%^&*(\"\n\nfunction character(value: string): KeyDefinition | undefined {\n if (value.length !== 1) return undefined\n if (/[a-z]/i.test(value)) return { key: value, code: `Key${value.toUpperCase()}`, keyCode: value.toUpperCase().charCodeAt(0), text: value }\n if (/[0-9]/.test(value)) return { key: value, code: `Digit${value}`, keyCode: value.charCodeAt(0), text: value }\n const shiftedDigit = SHIFTED_DIGITS.indexOf(value)\n if (shiftedDigit >= 0) return { key: value, code: `Digit${shiftedDigit}`, keyCode: 48 + shiftedDigit, text: value }\n for (const [base, [code, keyCode, shifted]] of Object.entries(PUNCTUATION)) {\n if (value === base || value === shifted) return { key: value, code, keyCode, text: value }\n }\n return undefined\n}\n\n/** Parses \"Enter\", \"a\", \"Shift+Tab\", \"Control+Shift+K\" or \"Mod+K\" (Meta on macOS, Control elsewhere). */\nexport function keyChord(chord: string, platform: NodeJS.Platform = process.platform): KeyEvent[] {\n const parts = chord === \"+\" ? [\"+\"] : chord.split(\"+\").map(part => part.trim())\n if (!parts.length || parts.some(part => !part) || chord.length > 64) throw new Error(`Unknown key ${JSON.stringify(chord)}`)\n const main = parts.pop()!\n const modifiers: string[] = []\n for (const part of parts) {\n const name = part.toLowerCase() === \"mod\" ? (platform === \"darwin\" ? \"Meta\" : \"Control\") : MODIFIER_ALIASES[part.toLowerCase()]\n if (!name) throw new Error(`Unknown modifier ${JSON.stringify(part)} in ${JSON.stringify(chord)}; use Control, Shift, Alt, Meta or Mod`)\n if (!modifiers.includes(name)) modifiers.push(name)\n }\n const named = NAMED[main] ?? NAMED[NAMED_ALIASES[main.toLowerCase()] ?? \"\"]\n ?? Object.values(NAMED).find(definition => definition.key.toLowerCase() === main.toLowerCase() && definition.key.length > 1)\n let definition = named ?? character(main)\n if (!definition) throw new Error(`Unknown key ${JSON.stringify(main)}; use a character or a key name such as Enter, Escape, Tab or ArrowDown`)\n // Shift+a types \"A\", like a physical keyboard.\n if (modifiers.includes(\"Shift\") && definition.text && /^[a-z]$/.test(definition.text)) {\n definition = { ...definition, key: definition.text.toUpperCase(), text: definition.text.toUpperCase() }\n }\n const events: KeyEvent[] = []\n let bits = 0\n for (const name of modifiers) {\n const { definition: modifier, bit } = MODIFIERS[name]\n bits |= bit\n events.push(keyEvent(\"rawKeyDown\", modifier, bits))\n }\n // A shortcut types nothing; a plain or shifted press produces its text.\n const text = bits & ~MODIFIERS.Shift.bit ? undefined : definition.text\n events.push(keyEvent(text ? \"keyDown\" : \"rawKeyDown\", definition, bits, text))\n events.push(keyEvent(\"keyUp\", definition, bits))\n for (const name of [...modifiers].reverse()) {\n const { definition: modifier, bit } = MODIFIERS[name]\n bits &= ~bit\n events.push(keyEvent(\"keyUp\", modifier, bits))\n }\n return events\n}\n\nfunction keyEvent(type: KeyEvent[\"type\"], definition: KeyDefinition, modifiers: number, text?: string): KeyEvent {\n return { type, key: definition.key, code: definition.code, windowsVirtualKeyCode: definition.keyCode, modifiers,\n ...(text ? { text, unmodifiedText: text } : {}), ...(definition.location ? { location: definition.location } : {}) }\n}\n"]}
@@ -0,0 +1,32 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+ import { keyChord } from "./keys";
4
+ const brief = (chord, platform = "win32") => keyChord(chord, platform).map(event => `${event.type}:${event.key}:${event.modifiers}${event.text ? `:${JSON.stringify(event.text)}` : ""}`);
5
+ test("plain keys type their text; named keys dispatch without text except Enter and Space", () => {
6
+ assert.deepEqual(brief("a"), ["keyDown:a:0:\"a\"", "keyUp:a:0"]);
7
+ assert.deepEqual(brief("Enter"), ["keyDown:Enter:0:\"\\r\"", "keyUp:Enter:0"]);
8
+ assert.deepEqual(brief("Space"), ["keyDown: :0:\" \"", "keyUp: :0"]);
9
+ assert.deepEqual(brief("Escape"), ["rawKeyDown:Escape:0", "keyUp:Escape:0"]);
10
+ assert.deepEqual(brief("esc"), brief("Escape"), "aliases and case-insensitive names");
11
+ assert.deepEqual(brief("arrowdown"), ["rawKeyDown:ArrowDown:0", "keyUp:ArrowDown:0"]);
12
+ assert.equal(keyChord("F5")[0].windowsVirtualKeyCode, 116);
13
+ assert.deepEqual(keyChord("/").map(event => event.code), ["Slash", "Slash"]);
14
+ });
15
+ test("chords hold modifiers around the key, type nothing, and release in reverse", () => {
16
+ assert.deepEqual(brief("Control+Shift+K"), [
17
+ "rawKeyDown:Control:2", "rawKeyDown:Shift:10", "rawKeyDown:K:10", "keyUp:K:10", "keyUp:Shift:2", "keyUp:Control:0"
18
+ ]);
19
+ assert.deepEqual(brief("Shift+Tab"), ["rawKeyDown:Shift:8", "rawKeyDown:Tab:8", "keyUp:Tab:8", "keyUp:Shift:0"]);
20
+ assert.deepEqual(brief("Shift+a"), ["rawKeyDown:Shift:8", "keyDown:A:8:\"A\"", "keyUp:A:8", "keyUp:Shift:0"], "shift types the capital");
21
+ assert.deepEqual(brief("Mod+k", "darwin").slice(0, 2), ["rawKeyDown:Meta:4", "rawKeyDown:k:4"]);
22
+ assert.deepEqual(brief("Mod+k", "linux").slice(0, 2), ["rawKeyDown:Control:2", "rawKeyDown:k:2"]);
23
+ assert.deepEqual(brief("ctrl+cmd+alt+x").slice(0, 3), ["rawKeyDown:Control:2", "rawKeyDown:Meta:6", "rawKeyDown:Alt:7"]);
24
+ assert.deepEqual(brief("+"), ["keyDown:+:0:\"+\"", "keyUp:+:0"]);
25
+ });
26
+ test("unknown keys and modifiers are refused with the accepted vocabulary", () => {
27
+ assert.throws(() => keyChord("Hyper+Q"), /Unknown modifier "Hyper".*Control, Shift, Alt, Meta or Mod/);
28
+ assert.throws(() => keyChord("Enterr"), /Unknown key "Enterr"; use a character or a key name/);
29
+ assert.throws(() => keyChord("Control+"), /Unknown key/);
30
+ assert.throws(() => keyChord("é"), /Unknown key "é"/);
31
+ });
32
+ //# sourceMappingURL=keys.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keys.test.js","sourceRoot":"","sources":["../../../src/automation/page/keys.test.ts"],"names":[],"mappings":"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","sourcesContent":["import assert from \"node:assert/strict\"\nimport { test } from \"node:test\"\nimport { keyChord } from \"./keys\"\n\nconst brief = (chord: string, platform: NodeJS.Platform = \"win32\") =>\n keyChord(chord, platform).map(event => `${event.type}:${event.key}:${event.modifiers}${event.text ? `:${JSON.stringify(event.text)}` : \"\"}`)\n\ntest(\"plain keys type their text; named keys dispatch without text except Enter and Space\", () => {\n assert.deepEqual(brief(\"a\"), [\"keyDown:a:0:\\\"a\\\"\", \"keyUp:a:0\"])\n assert.deepEqual(brief(\"Enter\"), [\"keyDown:Enter:0:\\\"\\\\r\\\"\", \"keyUp:Enter:0\"])\n assert.deepEqual(brief(\"Space\"), [\"keyDown: :0:\\\" \\\"\", \"keyUp: :0\"])\n assert.deepEqual(brief(\"Escape\"), [\"rawKeyDown:Escape:0\", \"keyUp:Escape:0\"])\n assert.deepEqual(brief(\"esc\"), brief(\"Escape\"), \"aliases and case-insensitive names\")\n assert.deepEqual(brief(\"arrowdown\"), [\"rawKeyDown:ArrowDown:0\", \"keyUp:ArrowDown:0\"])\n assert.equal(keyChord(\"F5\")[0].windowsVirtualKeyCode, 116)\n assert.deepEqual(keyChord(\"/\").map(event => event.code), [\"Slash\", \"Slash\"])\n})\n\ntest(\"chords hold modifiers around the key, type nothing, and release in reverse\", () => {\n assert.deepEqual(brief(\"Control+Shift+K\"), [\n \"rawKeyDown:Control:2\", \"rawKeyDown:Shift:10\", \"rawKeyDown:K:10\", \"keyUp:K:10\", \"keyUp:Shift:2\", \"keyUp:Control:0\"])\n assert.deepEqual(brief(\"Shift+Tab\"), [\"rawKeyDown:Shift:8\", \"rawKeyDown:Tab:8\", \"keyUp:Tab:8\", \"keyUp:Shift:0\"])\n assert.deepEqual(brief(\"Shift+a\"), [\"rawKeyDown:Shift:8\", \"keyDown:A:8:\\\"A\\\"\", \"keyUp:A:8\", \"keyUp:Shift:0\"], \"shift types the capital\")\n assert.deepEqual(brief(\"Mod+k\", \"darwin\").slice(0, 2), [\"rawKeyDown:Meta:4\", \"rawKeyDown:k:4\"])\n assert.deepEqual(brief(\"Mod+k\", \"linux\").slice(0, 2), [\"rawKeyDown:Control:2\", \"rawKeyDown:k:2\"])\n assert.deepEqual(brief(\"ctrl+cmd+alt+x\").slice(0, 3), [\"rawKeyDown:Control:2\", \"rawKeyDown:Meta:6\", \"rawKeyDown:Alt:7\"])\n assert.deepEqual(brief(\"+\"), [\"keyDown:+:0:\\\"+\\\"\", \"keyUp:+:0\"])\n})\n\ntest(\"unknown keys and modifiers are refused with the accepted vocabulary\", () => {\n assert.throws(() => keyChord(\"Hyper+Q\"), /Unknown modifier \"Hyper\".*Control, Shift, Alt, Meta or Mod/)\n assert.throws(() => keyChord(\"Enterr\"), /Unknown key \"Enterr\"; use a character or a key name/)\n assert.throws(() => keyChord(\"Control+\"), /Unknown key/)\n assert.throws(() => keyChord(\"é\"), /Unknown key \"é\"/)\n})\n"]}
@@ -0,0 +1,79 @@
1
+ import { keyChord } from "./keys";
2
+ export const PAGE_ACTIONS = ["click", "type", "select", "hover", "drag", "scroll", "press", "wait"];
3
+ export const MAX_WAIT_MS = 15_000;
4
+ const MAX_SELECT_VALUES = 64;
5
+ const MAX_TEXT_LENGTH = 200;
6
+ const MAX_SCROLL = 10_000;
7
+ /** Validates one page action; `undefined` when `action` names no page action. */
8
+ export function parsePageAction(value, inspect) {
9
+ const ref = (field = "ref") => {
10
+ if (typeof value[field] !== "string" || !value[field])
11
+ throw new Error(`${value.action} requires ${field}: a ref from the latest ${inspect}`);
12
+ return value[field];
13
+ };
14
+ const optionalRef = () => value.ref === undefined ? {} : { ref: ref() };
15
+ switch (value.action) {
16
+ case "click":
17
+ if (value.button !== undefined && value.button !== "left" && value.button !== "right")
18
+ throw new Error("click button must be left or right");
19
+ return { action: "click", ref: ref(), ...(value.button === "right" ? { button: "right" } : {}) };
20
+ case "type":
21
+ if (typeof value.text !== "string")
22
+ throw new Error("type requires ref and text");
23
+ if (value.clear !== undefined && typeof value.clear !== "boolean")
24
+ throw new Error("type clear must be true or false");
25
+ return { action: "type", ref: ref(), text: value.text, clear: value.clear !== false };
26
+ case "select":
27
+ if (!Array.isArray(value.values) || value.values.length > MAX_SELECT_VALUES || !value.values.every(item => typeof item === "string")) {
28
+ throw new Error("select requires ref and values: the option labels or values to choose");
29
+ }
30
+ return { action: "select", ref: ref(), values: value.values };
31
+ case "hover":
32
+ return { action: "hover", ref: ref() };
33
+ case "drag":
34
+ return { action: "drag", ref: ref(), to: ref("to") };
35
+ case "scroll": {
36
+ if (!["up", "down", "left", "right"].includes(value.direction))
37
+ throw new Error("scroll requires direction: up, down, left or right");
38
+ const amount = value.amount ?? 600;
39
+ if (typeof amount !== "number" || !Number.isInteger(amount) || amount < 1 || amount > MAX_SCROLL) {
40
+ throw new Error(`scroll amount must be a whole number of pixels from 1 to ${MAX_SCROLL}`);
41
+ }
42
+ return { action: "scroll", ...optionalRef(), direction: value.direction, amount };
43
+ }
44
+ case "press":
45
+ if (typeof value.key !== "string" || !value.key)
46
+ throw new Error("press requires key, such as Enter, Escape, Tab, ArrowDown or Control+K");
47
+ keyChord(value.key);
48
+ return { action: "press", key: value.key, ...optionalRef() };
49
+ case "wait": {
50
+ if (typeof value.text !== "string" || !value.text.trim() || value.text.length > MAX_TEXT_LENGTH) {
51
+ throw new Error(`wait requires text of at most ${MAX_TEXT_LENGTH} characters`);
52
+ }
53
+ if (value.gone !== undefined && typeof value.gone !== "boolean")
54
+ throw new Error("wait gone must be true or false");
55
+ const timeout = value.timeout ?? 5_000;
56
+ if (typeof timeout !== "number" || !Number.isInteger(timeout) || timeout < 0 || timeout > MAX_WAIT_MS) {
57
+ throw new Error(`wait timeout must be a whole number of milliseconds from 0 to ${MAX_WAIT_MS}`);
58
+ }
59
+ return { action: "wait", text: value.text, gone: value.gone === true, timeout };
60
+ }
61
+ default:
62
+ return undefined;
63
+ }
64
+ }
65
+ /** JSON Schema properties shared by the tools that accept page actions. */
66
+ export const PAGE_ACTION_PROPERTIES = {
67
+ ref: { type: "string", description: "Element ref from the latest inspection (optional for scroll and press)" },
68
+ text: { type: "string", description: "type: text to enter; wait: text to wait for" },
69
+ clear: { type: "boolean", description: "type only: replace the field's current text (default true); false appends" },
70
+ values: { type: "array", items: { type: "string" }, description: "select only: option labels or values to choose; one for an ordinary select" },
71
+ button: { type: "string", enum: ["left", "right"], description: "click only: mouse button (default left)" },
72
+ to: { type: "string", description: "drag only: ref of the element to drop onto" },
73
+ direction: { type: "string", enum: ["up", "down", "left", "right"], description: "scroll only" },
74
+ amount: { type: "integer", minimum: 1, maximum: MAX_SCROLL, description: "scroll only: pixels (default 600)" },
75
+ key: { type: "string", description: "press only: a key or chord such as Enter, Escape, Tab, Shift+Tab, ArrowDown, a, Control+K or Mod+K (Meta on macOS)" },
76
+ gone: { type: "boolean", description: "wait only: wait until the text disappears instead" },
77
+ timeout: { type: "integer", minimum: 0, maximum: MAX_WAIT_MS, description: "wait only: milliseconds (default 5000)" },
78
+ };
79
+ //# sourceMappingURL=parse.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse.js","sourceRoot":"","sources":["../../../src/automation/page/parse.ts"],"names":[],"mappings":"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","sourcesContent":["import { keyChord } from \"./keys\"\n\n/** Page actions as tools receive them, with elements as refs from the latest inspection. Parsing is\n * idempotent, so the same flat command crosses the bridge and is validated again by the backend. */\nexport type RefAction =\n | { action: \"click\"; ref: string; button?: \"left\" | \"right\" }\n | { action: \"type\"; ref: string; text: string; clear: boolean }\n | { action: \"select\"; ref: string; values: string[] }\n | { action: \"hover\"; ref: string }\n | { action: \"drag\"; ref: string; to: string }\n | { action: \"scroll\"; ref?: string; direction: \"up\" | \"down\" | \"left\" | \"right\"; amount: number }\n | { action: \"press\"; key: string; ref?: string }\n | { action: \"wait\"; text: string; gone: boolean; timeout: number }\n\nexport const PAGE_ACTIONS = [\"click\", \"type\", \"select\", \"hover\", \"drag\", \"scroll\", \"press\", \"wait\"] as const\nexport const MAX_WAIT_MS = 15_000\nconst MAX_SELECT_VALUES = 64\nconst MAX_TEXT_LENGTH = 200\nconst MAX_SCROLL = 10_000\n\n/** Validates one page action; `undefined` when `action` names no page action. */\nexport function parsePageAction(value: Record<string, unknown>, inspect: string): RefAction | undefined {\n const ref = (field = \"ref\") => {\n if (typeof value[field] !== \"string\" || !value[field]) throw new Error(`${value.action} requires ${field}: a ref from the latest ${inspect}`)\n return value[field] as string\n }\n const optionalRef = () => value.ref === undefined ? {} : { ref: ref() }\n switch (value.action) {\n case \"click\":\n if (value.button !== undefined && value.button !== \"left\" && value.button !== \"right\") throw new Error(\"click button must be left or right\")\n return { action: \"click\", ref: ref(), ...(value.button === \"right\" ? { button: \"right\" as const } : {}) }\n case \"type\":\n if (typeof value.text !== \"string\") throw new Error(\"type requires ref and text\")\n if (value.clear !== undefined && typeof value.clear !== \"boolean\") throw new Error(\"type clear must be true or false\")\n return { action: \"type\", ref: ref(), text: value.text, clear: value.clear !== false }\n case \"select\":\n if (!Array.isArray(value.values) || value.values.length > MAX_SELECT_VALUES || !value.values.every(item => typeof item === \"string\")) {\n throw new Error(\"select requires ref and values: the option labels or values to choose\")\n }\n return { action: \"select\", ref: ref(), values: value.values as string[] }\n case \"hover\":\n return { action: \"hover\", ref: ref() }\n case \"drag\":\n return { action: \"drag\", ref: ref(), to: ref(\"to\") }\n case \"scroll\": {\n if (![\"up\", \"down\", \"left\", \"right\"].includes(value.direction as string)) throw new Error(\"scroll requires direction: up, down, left or right\")\n const amount = value.amount ?? 600\n if (typeof amount !== \"number\" || !Number.isInteger(amount) || amount < 1 || amount > MAX_SCROLL) {\n throw new Error(`scroll amount must be a whole number of pixels from 1 to ${MAX_SCROLL}`)\n }\n return { action: \"scroll\", ...optionalRef(), direction: value.direction as \"up\" | \"down\" | \"left\" | \"right\", amount }\n }\n case \"press\":\n if (typeof value.key !== \"string\" || !value.key) throw new Error(\"press requires key, such as Enter, Escape, Tab, ArrowDown or Control+K\")\n keyChord(value.key)\n return { action: \"press\", key: value.key, ...optionalRef() }\n case \"wait\": {\n if (typeof value.text !== \"string\" || !value.text.trim() || value.text.length > MAX_TEXT_LENGTH) {\n throw new Error(`wait requires text of at most ${MAX_TEXT_LENGTH} characters`)\n }\n if (value.gone !== undefined && typeof value.gone !== \"boolean\") throw new Error(\"wait gone must be true or false\")\n const timeout = value.timeout ?? 5_000\n if (typeof timeout !== \"number\" || !Number.isInteger(timeout) || timeout < 0 || timeout > MAX_WAIT_MS) {\n throw new Error(`wait timeout must be a whole number of milliseconds from 0 to ${MAX_WAIT_MS}`)\n }\n return { action: \"wait\", text: value.text, gone: value.gone === true, timeout }\n }\n default:\n return undefined\n }\n}\n\n/** JSON Schema properties shared by the tools that accept page actions. */\nexport const PAGE_ACTION_PROPERTIES = {\n ref: { type: \"string\", description: \"Element ref from the latest inspection (optional for scroll and press)\" },\n text: { type: \"string\", description: \"type: text to enter; wait: text to wait for\" },\n clear: { type: \"boolean\", description: \"type only: replace the field's current text (default true); false appends\" },\n values: { type: \"array\", items: { type: \"string\" }, description: \"select only: option labels or values to choose; one for an ordinary select\" },\n button: { type: \"string\", enum: [\"left\", \"right\"], description: \"click only: mouse button (default left)\" },\n to: { type: \"string\", description: \"drag only: ref of the element to drop onto\" },\n direction: { type: \"string\", enum: [\"up\", \"down\", \"left\", \"right\"], description: \"scroll only\" },\n amount: { type: \"integer\", minimum: 1, maximum: MAX_SCROLL, description: \"scroll only: pixels (default 600)\" },\n key: { type: \"string\", description: \"press only: a key or chord such as Enter, Escape, Tab, Shift+Tab, ArrowDown, a, Control+K or Mod+K (Meta on macOS)\" },\n gone: { type: \"boolean\", description: \"wait only: wait until the text disappears instead\" },\n timeout: { type: \"integer\", minimum: 0, maximum: MAX_WAIT_MS, description: \"wait only: milliseconds (default 5000)\" },\n} as const\n"]}