@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.
- package/README.md +5 -0
- package/dist/agent-instructions/content.js +11 -0
- package/dist/agent-instructions/content.js.map +1 -1
- package/dist/cdp/client.d.ts +35 -0
- package/dist/cdp/client.js +149 -0
- package/dist/cdp/client.js.map +1 -0
- package/dist/cdp/flow.d.ts +178 -0
- package/dist/cdp/flow.js +217 -0
- package/dist/cdp/flow.js.map +1 -0
- package/dist/cdp/gate.d.ts +31 -0
- package/dist/cdp/gate.js +35 -0
- package/dist/cdp/gate.js.map +1 -0
- package/dist/cdp/grant.d.ts +45 -0
- package/dist/cdp/grant.js +81 -0
- package/dist/cdp/grant.js.map +1 -0
- package/dist/cdp/inpage/builders.d.ts +165 -0
- package/dist/cdp/inpage/builders.js +501 -0
- package/dist/cdp/inpage/builders.js.map +1 -0
- package/dist/cdp/inpage/deep-query.d.ts +57 -0
- package/dist/cdp/inpage/deep-query.js +325 -0
- package/dist/cdp/inpage/deep-query.js.map +1 -0
- package/dist/cdp/inpage/dom-helpers.d.ts +27 -0
- package/dist/cdp/inpage/dom-helpers.js +147 -0
- package/dist/cdp/inpage/dom-helpers.js.map +1 -0
- package/dist/cdp/keymap.d.ts +83 -0
- package/dist/cdp/keymap.js +205 -0
- package/dist/cdp/keymap.js.map +1 -0
- package/dist/cdp/settle.d.ts +38 -0
- package/dist/cdp/settle.js +76 -0
- package/dist/cdp/settle.js.map +1 -0
- package/dist/cdp/support.d.ts +26 -0
- package/dist/cdp/support.js +58 -0
- package/dist/cdp/support.js.map +1 -0
- package/dist/cdp/targets.d.ts +54 -0
- package/dist/cdp/targets.js +176 -0
- package/dist/cdp/targets.js.map +1 -0
- package/dist/cdp/transport.d.ts +31 -0
- package/dist/cdp/transport.js +531 -0
- package/dist/cdp/transport.js.map +1 -0
- package/dist/cli.js +50 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/about.js +13 -0
- package/dist/commands/about.js.map +1 -1
- package/dist/commands/cdp.d.ts +28 -0
- package/dist/commands/cdp.js +172 -0
- package/dist/commands/cdp.js.map +1 -0
- package/dist/commands/config.d.ts +92 -0
- package/dist/commands/config.js +173 -12
- package/dist/commands/config.js.map +1 -1
- package/dist/config.d.ts +70 -0
- package/dist/config.js +94 -1
- package/dist/config.js.map +1 -1
- package/dist/extension/manifest.json +1 -1
- package/dist/server.js +9 -0
- package/dist/server.js.map +1 -1
- package/dist/tools/browser-definitions.js +4 -3
- package/dist/tools/browser-definitions.js.map +1 -1
- package/dist/tools/browser-dispatch.d.ts +31 -0
- package/dist/tools/browser-dispatch.js +88 -40
- package/dist/tools/browser-dispatch.js.map +1 -1
- package/dist/tools/server-instructions.js +19 -0
- package/dist/tools/server-instructions.js.map +1 -1
- 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[];
|