@jobshimo/browser-link 0.22.0 → 0.23.0

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 (63) hide show
  1. package/README.md +5 -0
  2. package/dist/agent-instructions/content.js +11 -0
  3. package/dist/agent-instructions/content.js.map +1 -1
  4. package/dist/cdp/client.d.ts +35 -0
  5. package/dist/cdp/client.js +149 -0
  6. package/dist/cdp/client.js.map +1 -0
  7. package/dist/cdp/flow.d.ts +178 -0
  8. package/dist/cdp/flow.js +217 -0
  9. package/dist/cdp/flow.js.map +1 -0
  10. package/dist/cdp/gate.d.ts +31 -0
  11. package/dist/cdp/gate.js +35 -0
  12. package/dist/cdp/gate.js.map +1 -0
  13. package/dist/cdp/grant.d.ts +45 -0
  14. package/dist/cdp/grant.js +81 -0
  15. package/dist/cdp/grant.js.map +1 -0
  16. package/dist/cdp/inpage/builders.d.ts +165 -0
  17. package/dist/cdp/inpage/builders.js +501 -0
  18. package/dist/cdp/inpage/builders.js.map +1 -0
  19. package/dist/cdp/inpage/deep-query.d.ts +57 -0
  20. package/dist/cdp/inpage/deep-query.js +325 -0
  21. package/dist/cdp/inpage/deep-query.js.map +1 -0
  22. package/dist/cdp/inpage/dom-helpers.d.ts +27 -0
  23. package/dist/cdp/inpage/dom-helpers.js +147 -0
  24. package/dist/cdp/inpage/dom-helpers.js.map +1 -0
  25. package/dist/cdp/keymap.d.ts +83 -0
  26. package/dist/cdp/keymap.js +205 -0
  27. package/dist/cdp/keymap.js.map +1 -0
  28. package/dist/cdp/settle.d.ts +38 -0
  29. package/dist/cdp/settle.js +76 -0
  30. package/dist/cdp/settle.js.map +1 -0
  31. package/dist/cdp/support.d.ts +26 -0
  32. package/dist/cdp/support.js +58 -0
  33. package/dist/cdp/support.js.map +1 -0
  34. package/dist/cdp/targets.d.ts +54 -0
  35. package/dist/cdp/targets.js +176 -0
  36. package/dist/cdp/targets.js.map +1 -0
  37. package/dist/cdp/transport.d.ts +31 -0
  38. package/dist/cdp/transport.js +531 -0
  39. package/dist/cdp/transport.js.map +1 -0
  40. package/dist/cli.js +50 -0
  41. package/dist/cli.js.map +1 -1
  42. package/dist/commands/about.js +13 -0
  43. package/dist/commands/about.js.map +1 -1
  44. package/dist/commands/cdp.d.ts +28 -0
  45. package/dist/commands/cdp.js +172 -0
  46. package/dist/commands/cdp.js.map +1 -0
  47. package/dist/commands/config.d.ts +92 -0
  48. package/dist/commands/config.js +173 -12
  49. package/dist/commands/config.js.map +1 -1
  50. package/dist/config.d.ts +70 -0
  51. package/dist/config.js +94 -1
  52. package/dist/config.js.map +1 -1
  53. package/dist/extension/manifest.json +1 -1
  54. package/dist/server.js +9 -0
  55. package/dist/server.js.map +1 -1
  56. package/dist/tools/browser-definitions.js +4 -3
  57. package/dist/tools/browser-definitions.js.map +1 -1
  58. package/dist/tools/browser-dispatch.d.ts +31 -0
  59. package/dist/tools/browser-dispatch.js +88 -40
  60. package/dist/tools/browser-dispatch.js.map +1 -1
  61. package/dist/tools/server-instructions.js +19 -0
  62. package/dist/tools/server-instructions.js.map +1 -1
  63. package/package.json +1 -1
@@ -0,0 +1,325 @@
1
+ /**
2
+ * Independent copy of `packages/extension/src/inpage/deep-query.ts` — used
3
+ * by the cdp-direct transport (`../transport.ts`) to build the exact same
4
+ * `Runtime.evaluate` expressions the extension's `background.ts` sends,
5
+ * over a raw CDP-over-WS connection instead of `chrome.debugger`.
6
+ *
7
+ * Duplicated rather than imported for the same reason `config.ts` and
8
+ * `messages.ts` keep independent copies of extension-side constants/types:
9
+ * `@jobshimo/browser-link` (this package) publishes to npm standalone and
10
+ * must not carry a workspace dependency on `@browser-link/extension` (a
11
+ * private, unpublished package with `chrome.*` types in its own tsconfig).
12
+ * This file is copied VERBATIM — it has zero `chrome.*` references (it only
13
+ * builds a JS-source string that runs inside the TARGET PAGE, never the
14
+ * extension/server process), so there is nothing to adapt. If the source
15
+ * file changes, re-copy it here — the exported `DEEP_QUERY_JS` body is
16
+ * asserted byte-identical to the sibling at
17
+ * `packages/extension/src/inpage/deep-query.ts` by `cdp/drift.test.ts`,
18
+ * which fails the build if the two ever diverge (a security fix to one copy
19
+ * must reach the other).
20
+ *
21
+ * In-page JS source injected into CDP `Runtime.evaluate` expressions. This
22
+ * is the "deep search" layer that lets `snapshot`, `find`, `click` and
23
+ * `type` see past two boundaries plain `document.querySelector` /
24
+ * `document.querySelectorAll` cannot cross:
25
+ *
26
+ * - OPEN Shadow DOM roots (`element.attachShadow({ mode: 'open' })`),
27
+ * walked recursively (shadow roots nested inside shadow roots).
28
+ * - Same-origin `<iframe>` documents, walked recursively (iframes nested
29
+ * inside iframes, shadow roots nested inside iframes and vice versa).
30
+ *
31
+ * Exported as a raw string, not real TypeScript — it runs inside the
32
+ * TARGET PAGE's JS context via CDP `Runtime.evaluate`, not inside this
33
+ * process. Keep the syntax plain (`var`/`function`, no optional chaining
34
+ * assumptions) since it must run unmodified in whatever JS engine the
35
+ * connected tab uses.
36
+ *
37
+ * Known limitations (also documented on the MCP tool descriptions):
38
+ * - CLOSED shadow roots (`{ mode: 'closed' }`) are unreachable from page
39
+ * world JS — `element.shadowRoot` is `null` for them by design. There
40
+ * is no CDP-level workaround from `Runtime.evaluate`.
41
+ * - CROSS-ORIGIN iframes are unreachable — `contentDocument` throws or
42
+ * returns null under the same-origin policy. Access is wrapped in
43
+ * try/catch so a cross-origin frame is silently skipped rather than
44
+ * failing the whole query.
45
+ * - A plain CSS selector cannot distinguish between two elements that
46
+ * produce byte-identical generated selectors in two different roots
47
+ * (e.g. two structurally-identical Shadow DOM component instances).
48
+ * `deepQueryFirst` returns the first match in traversal order
49
+ * (the start root itself, then nested shadow roots, then nested
50
+ * same-origin iframe documents, depth-first). Extremely rare in
51
+ * practice — real content differs — but worth knowing when a page
52
+ * repeats the exact same component markup in multiple places. When
53
+ * selector generation cannot avoid this (see `genSelectorInfo` in
54
+ * `dom-helpers.ts`), snapshot/find mark the affected entry with
55
+ * `ambiguous: true` so agents know not to cache that selector.
56
+ */
57
+ export const DEEP_QUERY_JS = `
58
+ function collectSearchRoots(start) {
59
+ // start: a Document, Element or ShadowRoot to scope the search to.
60
+ // Returns [start, ...every open shadow root and same-origin iframe
61
+ // document reachable inside start's subtree, recursively]. The caller
62
+ // runs .querySelectorAll()/.querySelector() on each entry directly —
63
+ // each entry's own querySelectorAll does NOT pierce into nested shadow
64
+ // roots or iframes on its own, which is why we enumerate them here.
65
+ var roots = [start];
66
+ function walk(scope, depth) {
67
+ // Depth guard, same pattern as the other recursive helpers here:
68
+ // pathological nesting (or a cycle introduced by a broken mock)
69
+ // must degrade to a truncated result, never a stack overflow.
70
+ if (depth > 20) return;
71
+ var all;
72
+ try {
73
+ all = scope.querySelectorAll('*');
74
+ } catch (_) {
75
+ return;
76
+ }
77
+ for (var i = 0; i < all.length; i++) {
78
+ var el = all[i];
79
+ if (el.shadowRoot) {
80
+ roots.push(el.shadowRoot);
81
+ walk(el.shadowRoot, depth + 1);
82
+ }
83
+ if (el.tagName === 'IFRAME') {
84
+ var doc = null;
85
+ try {
86
+ doc = el.contentDocument;
87
+ } catch (_) {
88
+ doc = null;
89
+ }
90
+ if (doc) {
91
+ roots.push(doc);
92
+ walk(doc, depth + 1);
93
+ }
94
+ }
95
+ }
96
+ }
97
+ walk(start, 0);
98
+ return roots;
99
+ }
100
+
101
+ function deepWalkAll(callback, start) {
102
+ // Tree-walk primitive: invoke callback(el) for every element reachable
103
+ // from start (default: document) across the top document, every open
104
+ // shadow root, and every same-origin iframe document.
105
+ var root = start || document;
106
+ var roots = collectSearchRoots(root);
107
+ for (var r = 0; r < roots.length; r++) {
108
+ var all;
109
+ try {
110
+ all = roots[r].querySelectorAll('*');
111
+ } catch (_) {
112
+ continue;
113
+ }
114
+ for (var i = 0; i < all.length; i++) callback(all[i]);
115
+ }
116
+ }
117
+
118
+ function deepQueryAll(selector, start) {
119
+ var root = start || document;
120
+ var roots = collectSearchRoots(root);
121
+ var out = [];
122
+ for (var r = 0; r < roots.length; r++) {
123
+ var found;
124
+ try {
125
+ found = roots[r].querySelectorAll(selector);
126
+ } catch (_) {
127
+ continue;
128
+ }
129
+ for (var i = 0; i < found.length; i++) out.push(found[i]);
130
+ }
131
+ return out;
132
+ }
133
+
134
+ function deepQueryFirst(selector, start) {
135
+ var root = start || document;
136
+ var roots = collectSearchRoots(root);
137
+ for (var r = 0; r < roots.length; r++) {
138
+ var el = null;
139
+ try {
140
+ el = roots[r].querySelector(selector);
141
+ } catch (_) {
142
+ el = null;
143
+ }
144
+ if (el) return el;
145
+ }
146
+ return null;
147
+ }
148
+
149
+ function frameElementChain(el) {
150
+ // Walk up from el's own window to the top window, collecting the
151
+ // <iframe> element that hosts each intermediate document. Outermost
152
+ // frame first. Same-origin only — frameElement throws/returns null
153
+ // across an origin boundary, at which point the chain simply stops
154
+ // (the coordinate math below degrades gracefully rather than throwing).
155
+ var chain = [];
156
+ var doc = el.ownerDocument;
157
+ var win = doc && doc.defaultView;
158
+ if (!win) return chain;
159
+ var topWin;
160
+ try {
161
+ topWin = win.top;
162
+ } catch (_) {
163
+ topWin = win;
164
+ }
165
+ var guard = 0;
166
+ while (win && win !== topWin && guard < 20) {
167
+ guard++;
168
+ var fe = null;
169
+ try {
170
+ fe = win.frameElement;
171
+ } catch (_) {
172
+ fe = null;
173
+ }
174
+ if (!fe) break;
175
+ chain.unshift(fe);
176
+ win = fe.ownerDocument && fe.ownerDocument.defaultView;
177
+ }
178
+ return chain;
179
+ }
180
+
181
+ function frameViewportOriginOffset(fe) {
182
+ // The embedded document's viewport origin sits at the frame element's
183
+ // CONTENT box, but getBoundingClientRect() returns the BORDER box —
184
+ // border and padding must be added on top of rect.left/rect.top or
185
+ // every coordinate inside a styled iframe lands short. clientLeft/
186
+ // clientTop are the border widths; padding comes from computed style
187
+ // (read through the frame's own view — same-origin, so reachable).
188
+ var borderX = fe.clientLeft || 0;
189
+ var borderY = fe.clientTop || 0;
190
+ var padX = 0;
191
+ var padY = 0;
192
+ try {
193
+ var view = (fe.ownerDocument && fe.ownerDocument.defaultView) || window;
194
+ var style = view.getComputedStyle(fe);
195
+ padX = parseFloat(style.paddingLeft) || 0;
196
+ padY = parseFloat(style.paddingTop) || 0;
197
+ } catch (_) {}
198
+ return { x: borderX + padX, y: borderY + padY };
199
+ }
200
+
201
+ function clickRectOf(el) {
202
+ // For a line-wrapped INLINE element (a link spanning two lines), the
203
+ // center of getBoundingClientRect() can land in the unpainted gap
204
+ // between the line boxes — elementFromPoint would report whatever sits
205
+ // behind, and a dispatched click would miss. The first client rect is
206
+ // always a painted fragment, so its center is a safe click point.
207
+ // Block-level elements have exactly one client rect, identical to the
208
+ // bounding rect — no behavior change for them.
209
+ var rects = null;
210
+ try {
211
+ rects = el.getClientRects ? el.getClientRects() : null;
212
+ } catch (_) {
213
+ rects = null;
214
+ }
215
+ if (rects && rects.length > 0) return rects[0];
216
+ return el.getBoundingClientRect();
217
+ }
218
+
219
+ function viewportCenterOf(el) {
220
+ // Returns the element's click point in two coordinate spaces:
221
+ // - localX/localY: relative to el's OWN document viewport (what
222
+ // that document's elementFromPoint expects).
223
+ // - x/y: relative to the TOP-LEVEL viewport (what CDP
224
+ // Input.dispatchMouseEvent expects) — localX/localY plus the
225
+ // accumulated content-box origin of every ancestor <iframe>
226
+ // element (border-box rect + border + padding). Shadow DOM
227
+ // boundaries need no offset: they do not introduce a new
228
+ // coordinate space.
229
+ // Both spaces derive from the SAME point (first-client-rect center),
230
+ // so the occlusion hit-test and the CDP dispatch always agree.
231
+ var rect = clickRectOf(el);
232
+ var localX = rect.left + rect.width / 2;
233
+ var localY = rect.top + rect.height / 2;
234
+ var chain = frameElementChain(el);
235
+ var offsetX = 0;
236
+ var offsetY = 0;
237
+ for (var i = 0; i < chain.length; i++) {
238
+ var frameRect = chain[i].getBoundingClientRect();
239
+ var origin = frameViewportOriginOffset(chain[i]);
240
+ offsetX += frameRect.left + origin.x;
241
+ offsetY += frameRect.top + origin.y;
242
+ }
243
+ return { x: offsetX + localX, y: offsetY + localY, localX: localX, localY: localY };
244
+ }
245
+
246
+ function composedContains(ancestor, node) {
247
+ // Node.contains() does not cross shadow boundaries. This walks the
248
+ // COMPOSED tree: when climbing hits a ShadowRoot (no parentNode), jump
249
+ // to its host and keep climbing.
250
+ var cur = node;
251
+ var guard = 0;
252
+ while (cur && guard < 1000) {
253
+ guard++;
254
+ if (cur === ancestor) return true;
255
+ if (cur.parentNode) {
256
+ cur = cur.parentNode;
257
+ continue;
258
+ }
259
+ if (cur.host) {
260
+ cur = cur.host;
261
+ continue;
262
+ }
263
+ break;
264
+ }
265
+ return false;
266
+ }
267
+
268
+ function describeElement(el) {
269
+ if (!el || !el.tagName) return 'an unknown element';
270
+ var desc = el.tagName.toLowerCase();
271
+ if (el.id) desc += '#' + el.id;
272
+ if (el.classList && el.classList.length) {
273
+ var classes = [];
274
+ for (var i = 0; i < el.classList.length && i < 2; i++) classes.push(el.classList[i]);
275
+ if (classes.length) desc += '.' + classes.join('.');
276
+ }
277
+ return desc;
278
+ }
279
+
280
+ function hitTestDeep(root, x, y) {
281
+ var node = null;
282
+ try {
283
+ node = root && root.elementFromPoint ? root.elementFromPoint(x, y) : null;
284
+ } catch (_) {
285
+ node = null;
286
+ }
287
+ // The hit element may itself host a nested open shadow root whose
288
+ // content visually covers the point — descend until elementFromPoint
289
+ // stops finding a deeper shadow tree.
290
+ var guard = 0;
291
+ while (node && node.shadowRoot && guard < 20) {
292
+ guard++;
293
+ var inner = null;
294
+ try {
295
+ inner = node.shadowRoot.elementFromPoint ? node.shadowRoot.elementFromPoint(x, y) : null;
296
+ } catch (_) {
297
+ inner = null;
298
+ }
299
+ if (!inner || inner === node) break;
300
+ node = inner;
301
+ }
302
+ return node;
303
+ }
304
+
305
+ function checkOcclusion(target, localX, localY) {
306
+ // Hit-test starting from the element's OWN root (its closest shadow
307
+ // root, or the document that owns it when not in shadow DOM) using
308
+ // LOCAL coordinates — the same coordinate space that root's own
309
+ // elementFromPoint expects (an iframe document's viewport starts at
310
+ // its own top-left, not the top-level page's).
311
+ var root = target.getRootNode ? target.getRootNode() : document;
312
+ var hit = hitTestDeep(root, localX, localY);
313
+ if (!hit) {
314
+ // Nothing hit-tested at all (e.g. elementFromPoint unsupported in
315
+ // this environment, or the point is outside the rendered area).
316
+ // Fail open rather than block a click we cannot actually verify.
317
+ return { allowed: true };
318
+ }
319
+ if (hit === target || composedContains(target, hit) || composedContains(hit, target)) {
320
+ return { allowed: true };
321
+ }
322
+ return { allowed: false, blocker: describeElement(hit) };
323
+ }
324
+ `;
325
+ //# sourceMappingURL=deep-query.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deep-query.js","sourceRoot":"","sources":["../../../src/cdp/inpage/deep-query.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2Q5B,CAAC"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Independent copy of `packages/extension/src/inpage/dom-helpers.ts` — see
3
+ * `./deep-query.ts`'s header comment for why this is a verbatim duplicate
4
+ * rather than a cross-package import. The exported `DOM_HELPERS_JS` body is
5
+ * asserted byte-identical to that sibling by `cdp/drift.test.ts`.
6
+ *
7
+ * Shared in-page DOM helpers, injected verbatim into any JS template that
8
+ * needs them (`buildSnapshotJs`, `buildFindJs`). Defining them once here
9
+ * keeps the heuristics (visibility, selector generation, accessible text)
10
+ * identical across tools so a selector returned by `snapshot` and a selector
11
+ * returned by `find` follow the same rules.
12
+ *
13
+ * `genSelectorInfo` calls `deepQueryAll` to verify a candidate selector is
14
+ * unique across the FULL deep search scope (top document + every open
15
+ * shadow root + every same-origin iframe), not just `document`. That is a
16
+ * runtime dependency, not an import-time one — these are plain string
17
+ * templates concatenated at the CDP-expression build site, not real ES
18
+ * modules. Always interpolate `DEEP_QUERY_JS` (from `./deep-query.js`)
19
+ * BEFORE this constant in any expression that calls `genSelectorInfo`.
20
+ *
21
+ * When even a fully-qualified structural path stays ambiguous (two
22
+ * structurally-identical twins in different roots — no CSS syntax can
23
+ * scope to one root), `genSelectorInfo` returns `ambiguous: true` and the
24
+ * snapshot/find builders surface it on the affected entry so agents know
25
+ * that selector resolves first-match-wins and must not be cached.
26
+ */
27
+ export declare const DOM_HELPERS_JS = "\n function isVisible(el) {\n if (!(el instanceof HTMLElement)) return true;\n const style = getComputedStyle(el);\n if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return false;\n if (el.offsetParent === null && style.position !== 'fixed') return false;\n return true;\n }\n function shortText(el) {\n const t = (el.innerText || el.textContent || '').trim();\n return t.length > 120 ? t.slice(0, 120) + '...' : t;\n }\n function safeCss(s) {\n return s.replace(/\"/g, '\\\\\"');\n }\n function genSelectorInfo(el) {\n // Uniqueness is verified with deepQueryAll \u2014 an id/testid/aria-label/\n // name that is unique in the top document can still collide with an\n // identically-attributed element inside a shadow root or same-origin\n // iframe, since each root has its own independent attribute namespace.\n if (el.id && !/^[\\d]/.test(el.id) && !/\\s/.test(el.id)) {\n try {\n const candidate = '#' + CSS.escape(el.id);\n if (deepQueryAll(candidate).length === 1) return { selector: candidate, ambiguous: false };\n } catch (_) {}\n }\n const tid = el.getAttribute('data-testid');\n if (tid) {\n const candidate = '[data-testid=\"' + safeCss(tid) + '\"]';\n if (deepQueryAll(candidate).length === 1) return { selector: candidate, ambiguous: false };\n }\n const al = el.getAttribute('aria-label');\n if (al && al.length < 60) {\n const candidate = el.tagName.toLowerCase() + '[aria-label=\"' + safeCss(al) + '\"]';\n if (deepQueryAll(candidate).length === 1) return { selector: candidate, ambiguous: false };\n }\n const name = el.getAttribute('name');\n if (name && (el.tagName === 'INPUT' || el.tagName === 'SELECT' || el.tagName === 'TEXTAREA')) {\n const candidate = el.tagName.toLowerCase() + '[name=\"' + safeCss(name) + '\"]';\n if (deepQueryAll(candidate).length === 1) return { selector: candidate, ambiguous: false };\n }\n // Structural fallback, verified like every shortcut above. The short\n // form (6 parts, indexed only where siblings force it) is preferred\n // for stability; when it matches more than one element across roots,\n // retry with a fully-qualified path (every level :nth-of-type-indexed,\n // climbed all the way to the root boundary).\n const structural = buildStructuralSelector(el, 6, false);\n try {\n if (deepQueryAll(structural).length === 1) return { selector: structural, ambiguous: false };\n } catch (_) {}\n const qualified = buildStructuralSelector(el, 32, true);\n try {\n if (deepQueryAll(qualified).length === 1) return { selector: qualified, ambiguous: false };\n } catch (_) {}\n // Still ambiguous: structurally-identical twins in different roots.\n // No CSS syntax can scope a selector to one shadow root / iframe, so\n // return the most qualified path and flag it \u2014 deepQueryFirst will\n // resolve it first-match-wins (deterministic traversal order), and\n // the flag tells agents not to cache it.\n return { selector: qualified, ambiguous: true };\n }\n function genSelector(el) {\n return genSelectorInfo(el).selector;\n }\n function buildStructuralSelector(el, maxParts, alwaysIndex) {\n // Local-root-scoped structural path: CLIMBING stops at a shadow root or\n // document boundary (parentElement is null for a shadow root's direct\n // children) \u2014 matches how deepQueryAll scopes each root's own\n // querySelectorAll call, so the path resolves back to the same element\n // when re-queried through deepQueryFirst. Sibling lookup for the\n // :nth-of-type qualifier uses parentNode rather than parentElement so a\n // shadow root's own top-level children (parentNode = the ShadowRoot,\n // which still exposes .children) get disambiguated too, even though we\n // do not climb past that boundary. With alwaysIndex, every level gets\n // an explicit :nth-of-type \u2014 maximum qualification for the retry pass.\n const parts = [];\n let cur = el;\n while (cur && cur.nodeType === 1 && cur !== document.body && parts.length < maxParts) {\n let part = cur.tagName.toLowerCase();\n const parentNode = cur.parentNode;\n if (parentNode && parentNode.children) {\n const sib = Array.from(parentNode.children).filter(s => s.tagName === cur.tagName);\n if (sib.length > 1 || alwaysIndex) {\n part += ':nth-of-type(' + (sib.indexOf(cur) + 1) + ')';\n }\n }\n parts.unshift(part);\n cur = cur.parentElement;\n }\n return parts.join(' > ');\n }\n function accessibleText(el) {\n const aria = el.getAttribute && el.getAttribute('aria-label');\n if (aria) return aria;\n const txt = (el.innerText || el.textContent || '').trim();\n if (txt) return txt;\n if ('value' in el && el.value) return String(el.value);\n const ph = el.getAttribute && el.getAttribute('placeholder');\n if (ph) return ph;\n const title = el.getAttribute && el.getAttribute('title');\n if (title) return title;\n return '';\n }\n function frameSelectorFor(el) {\n // Lightweight context field for snapshot/find entries: the selector of\n // the innermost same-origin <iframe> hosting el, or null when el lives\n // in the top document. Computed via genSelector so it benefits from the\n // same deep-uniqueness verification.\n if (el.ownerDocument === document) return null;\n const win = el.ownerDocument && el.ownerDocument.defaultView;\n let fe = null;\n try {\n fe = win && win.frameElement;\n } catch (_) {\n fe = null;\n }\n if (!fe) return null;\n return genSelector(fe);\n }\n";
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Independent copy of `packages/extension/src/inpage/dom-helpers.ts` — see
3
+ * `./deep-query.ts`'s header comment for why this is a verbatim duplicate
4
+ * rather than a cross-package import. The exported `DOM_HELPERS_JS` body is
5
+ * asserted byte-identical to that sibling by `cdp/drift.test.ts`.
6
+ *
7
+ * Shared in-page DOM helpers, injected verbatim into any JS template that
8
+ * needs them (`buildSnapshotJs`, `buildFindJs`). Defining them once here
9
+ * keeps the heuristics (visibility, selector generation, accessible text)
10
+ * identical across tools so a selector returned by `snapshot` and a selector
11
+ * returned by `find` follow the same rules.
12
+ *
13
+ * `genSelectorInfo` calls `deepQueryAll` to verify a candidate selector is
14
+ * unique across the FULL deep search scope (top document + every open
15
+ * shadow root + every same-origin iframe), not just `document`. That is a
16
+ * runtime dependency, not an import-time one — these are plain string
17
+ * templates concatenated at the CDP-expression build site, not real ES
18
+ * modules. Always interpolate `DEEP_QUERY_JS` (from `./deep-query.js`)
19
+ * BEFORE this constant in any expression that calls `genSelectorInfo`.
20
+ *
21
+ * When even a fully-qualified structural path stays ambiguous (two
22
+ * structurally-identical twins in different roots — no CSS syntax can
23
+ * scope to one root), `genSelectorInfo` returns `ambiguous: true` and the
24
+ * snapshot/find builders surface it on the affected entry so agents know
25
+ * that selector resolves first-match-wins and must not be cached.
26
+ */
27
+ export const DOM_HELPERS_JS = `
28
+ function isVisible(el) {
29
+ if (!(el instanceof HTMLElement)) return true;
30
+ const style = getComputedStyle(el);
31
+ if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return false;
32
+ if (el.offsetParent === null && style.position !== 'fixed') return false;
33
+ return true;
34
+ }
35
+ function shortText(el) {
36
+ const t = (el.innerText || el.textContent || '').trim();
37
+ return t.length > 120 ? t.slice(0, 120) + '...' : t;
38
+ }
39
+ function safeCss(s) {
40
+ return s.replace(/"/g, '\\\\"');
41
+ }
42
+ function genSelectorInfo(el) {
43
+ // Uniqueness is verified with deepQueryAll — an id/testid/aria-label/
44
+ // name that is unique in the top document can still collide with an
45
+ // identically-attributed element inside a shadow root or same-origin
46
+ // iframe, since each root has its own independent attribute namespace.
47
+ if (el.id && !/^[\\d]/.test(el.id) && !/\\s/.test(el.id)) {
48
+ try {
49
+ const candidate = '#' + CSS.escape(el.id);
50
+ if (deepQueryAll(candidate).length === 1) return { selector: candidate, ambiguous: false };
51
+ } catch (_) {}
52
+ }
53
+ const tid = el.getAttribute('data-testid');
54
+ if (tid) {
55
+ const candidate = '[data-testid="' + safeCss(tid) + '"]';
56
+ if (deepQueryAll(candidate).length === 1) return { selector: candidate, ambiguous: false };
57
+ }
58
+ const al = el.getAttribute('aria-label');
59
+ if (al && al.length < 60) {
60
+ const candidate = el.tagName.toLowerCase() + '[aria-label="' + safeCss(al) + '"]';
61
+ if (deepQueryAll(candidate).length === 1) return { selector: candidate, ambiguous: false };
62
+ }
63
+ const name = el.getAttribute('name');
64
+ if (name && (el.tagName === 'INPUT' || el.tagName === 'SELECT' || el.tagName === 'TEXTAREA')) {
65
+ const candidate = el.tagName.toLowerCase() + '[name="' + safeCss(name) + '"]';
66
+ if (deepQueryAll(candidate).length === 1) return { selector: candidate, ambiguous: false };
67
+ }
68
+ // Structural fallback, verified like every shortcut above. The short
69
+ // form (6 parts, indexed only where siblings force it) is preferred
70
+ // for stability; when it matches more than one element across roots,
71
+ // retry with a fully-qualified path (every level :nth-of-type-indexed,
72
+ // climbed all the way to the root boundary).
73
+ const structural = buildStructuralSelector(el, 6, false);
74
+ try {
75
+ if (deepQueryAll(structural).length === 1) return { selector: structural, ambiguous: false };
76
+ } catch (_) {}
77
+ const qualified = buildStructuralSelector(el, 32, true);
78
+ try {
79
+ if (deepQueryAll(qualified).length === 1) return { selector: qualified, ambiguous: false };
80
+ } catch (_) {}
81
+ // Still ambiguous: structurally-identical twins in different roots.
82
+ // No CSS syntax can scope a selector to one shadow root / iframe, so
83
+ // return the most qualified path and flag it — deepQueryFirst will
84
+ // resolve it first-match-wins (deterministic traversal order), and
85
+ // the flag tells agents not to cache it.
86
+ return { selector: qualified, ambiguous: true };
87
+ }
88
+ function genSelector(el) {
89
+ return genSelectorInfo(el).selector;
90
+ }
91
+ function buildStructuralSelector(el, maxParts, alwaysIndex) {
92
+ // Local-root-scoped structural path: CLIMBING stops at a shadow root or
93
+ // document boundary (parentElement is null for a shadow root's direct
94
+ // children) — matches how deepQueryAll scopes each root's own
95
+ // querySelectorAll call, so the path resolves back to the same element
96
+ // when re-queried through deepQueryFirst. Sibling lookup for the
97
+ // :nth-of-type qualifier uses parentNode rather than parentElement so a
98
+ // shadow root's own top-level children (parentNode = the ShadowRoot,
99
+ // which still exposes .children) get disambiguated too, even though we
100
+ // do not climb past that boundary. With alwaysIndex, every level gets
101
+ // an explicit :nth-of-type — maximum qualification for the retry pass.
102
+ const parts = [];
103
+ let cur = el;
104
+ while (cur && cur.nodeType === 1 && cur !== document.body && parts.length < maxParts) {
105
+ let part = cur.tagName.toLowerCase();
106
+ const parentNode = cur.parentNode;
107
+ if (parentNode && parentNode.children) {
108
+ const sib = Array.from(parentNode.children).filter(s => s.tagName === cur.tagName);
109
+ if (sib.length > 1 || alwaysIndex) {
110
+ part += ':nth-of-type(' + (sib.indexOf(cur) + 1) + ')';
111
+ }
112
+ }
113
+ parts.unshift(part);
114
+ cur = cur.parentElement;
115
+ }
116
+ return parts.join(' > ');
117
+ }
118
+ function accessibleText(el) {
119
+ const aria = el.getAttribute && el.getAttribute('aria-label');
120
+ if (aria) return aria;
121
+ const txt = (el.innerText || el.textContent || '').trim();
122
+ if (txt) return txt;
123
+ if ('value' in el && el.value) return String(el.value);
124
+ const ph = el.getAttribute && el.getAttribute('placeholder');
125
+ if (ph) return ph;
126
+ const title = el.getAttribute && el.getAttribute('title');
127
+ if (title) return title;
128
+ return '';
129
+ }
130
+ function frameSelectorFor(el) {
131
+ // Lightweight context field for snapshot/find entries: the selector of
132
+ // the innermost same-origin <iframe> hosting el, or null when el lives
133
+ // in the top document. Computed via genSelector so it benefits from the
134
+ // same deep-uniqueness verification.
135
+ if (el.ownerDocument === document) return null;
136
+ const win = el.ownerDocument && el.ownerDocument.defaultView;
137
+ let fe = null;
138
+ try {
139
+ fe = win && win.frameElement;
140
+ } catch (_) {
141
+ fe = null;
142
+ }
143
+ if (!fe) return null;
144
+ return genSelector(fe);
145
+ }
146
+ `;
147
+ //# sourceMappingURL=dom-helpers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dom-helpers.js","sourceRoot":"","sources":["../../../src/cdp/inpage/dom-helpers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuH7B,CAAC"}
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Independent copy of `packages/extension/src/keymap.ts` — see
3
+ * `inpage/deep-query.ts`'s header comment for why this package duplicates
4
+ * rather than imports extension-side modules. Byte-identical logic;
5
+ * `cdp/drift.test.ts` asserts the `MODIFIER_BITS` mapping stays in lockstep
6
+ * with the sibling at `packages/extension/src/keymap.ts`.
7
+ *
8
+ * Keyboard key definitions for CDP `Input.dispatchKeyEvent`, driving
9
+ * `browser.press` over the cdp-direct transport (`./transport.ts`).
10
+ *
11
+ * Modeled after Puppeteer's `USKeyboardLayout` (US keyboard — the only
12
+ * layout CDP's `Input.dispatchKeyEvent` needs; Chrome maps
13
+ * `windowsVirtualKeyCode` + `code` to the OS's actual layout on its own),
14
+ * trimmed to what `browser.press` exposes: the named control/navigation
15
+ * keys plus any single printable character.
16
+ */
17
+ export interface KeyDefinition {
18
+ /** DOM `KeyboardEvent.key` value. */
19
+ key: string;
20
+ /** DOM `KeyboardEvent.code` value — physical key identity. */
21
+ code: string;
22
+ /** CDP `windowsVirtualKeyCode` (mirrored into `nativeVirtualKeyCode`).
23
+ * Chrome derives the platform-native key from this plus `code` — the
24
+ * same numeric value works on every OS. */
25
+ keyCode: number;
26
+ /** Text CDP should insert via a `char` event. Only set for keys that
27
+ * produce visible input (printable characters, Space). Control keys
28
+ * (Enter, Escape, Tab, arrows, …) omit it — they dispatch keyDown/keyUp
29
+ * only, no `char` event, matching real hardware. */
30
+ text?: string;
31
+ /** True when producing this exact character requires the Shift modifier
32
+ * on a US layout (e.g. "@" is Shift+2, "_" is Shift+-). The caller ORs
33
+ * this into the dispatched modifiers bitmask so `event.shiftKey`
34
+ * reports correctly even when the caller only asked for the character,
35
+ * not the physical chord. */
36
+ needsShift?: boolean;
37
+ }
38
+ /**
39
+ * Resolve a human-provided `key` name (the `browser.press` `key` param)
40
+ * into CDP dispatch parameters. Accepts:
41
+ * - a named control/navigation key, case-insensitive ("Enter", "arrowup"),
42
+ * - a single printable character, case-SENSITIVE ("a" vs "A", "@").
43
+ *
44
+ * Returns `null` when the name is neither — the caller surfaces that as a
45
+ * validation error instead of silently no-op'ing on an unrecognized key.
46
+ */
47
+ export declare function resolveKey(name: string): KeyDefinition | null;
48
+ /** CDP `Input.dispatchKeyEvent` modifiers bitmask. */
49
+ export declare const MODIFIER_BITS: Readonly<Record<string, number>>;
50
+ /** Fold a `modifiers` array (as accepted by `browser.press`) into the CDP
51
+ * bitmask. Unknown entries are ignored defensively — the server-side
52
+ * dispatcher already validates the enum, this is a second line of
53
+ * defence for direct/test callers. */
54
+ export declare function modifiersToBitmask(modifiers: readonly string[] | undefined): number;
55
+ /** One CDP `Input.dispatchKeyEvent` command payload. A type alias (not an
56
+ * interface) so it carries an implicit index signature and is directly
57
+ * assignable to the `Record<string, unknown>` the transport's `cdp()`
58
+ * helper takes. */
59
+ export type CdpKeyEvent = {
60
+ type: 'keyDown' | 'rawKeyDown' | 'char' | 'keyUp';
61
+ modifiers: number;
62
+ key: string;
63
+ code: string;
64
+ windowsVirtualKeyCode: number;
65
+ nativeVirtualKeyCode: number;
66
+ text?: string;
67
+ unmodifiedText?: string;
68
+ };
69
+ /**
70
+ * Build the CDP `Input.dispatchKeyEvent` sequence for one key press.
71
+ * Pure function so the exact event shapes are unit-testable.
72
+ *
73
+ * Shape: a text-bearing key (printable characters, Space, Enter) gets a
74
+ * `keyDown` (identity only, no `text` field — Chrome would insert text
75
+ * from a text-bearing keyDown AND from a following char event, doubling
76
+ * the input) followed by a separate `char` event carrying the text — the
77
+ * `char` event is what produces the KEYPRESS whose default action drives
78
+ * text insertion and, for Enter ('\r'), the WHATWG implicit form
79
+ * submission. A control key with no text (Escape, Tab, arrows, …) gets
80
+ * `rawKeyDown` with nothing to follow but `keyUp`, matching real hardware
81
+ * (those keys never fire keypress in Blink).
82
+ */
83
+ export declare function buildKeyEventSequence(def: KeyDefinition, modifiers: number): CdpKeyEvent[];