@jobshimo/browser-link 0.36.0 → 0.37.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/dist/agent-instructions/content.js +14 -3
- package/dist/agent-instructions/content.js.map +1 -1
- package/dist/cdp/flow.d.ts +36 -3
- package/dist/cdp/flow.js +54 -19
- package/dist/cdp/flow.js.map +1 -1
- package/dist/cdp/inpage/builders.d.ts +103 -20
- package/dist/cdp/inpage/builders.js +199 -52
- package/dist/cdp/inpage/builders.js.map +1 -1
- package/dist/cdp/inpage/observer.d.ts +36 -7
- package/dist/cdp/inpage/observer.js +242 -16
- package/dist/cdp/inpage/observer.js.map +1 -1
- package/dist/cdp/transport.js +90 -9
- package/dist/cdp/transport.js.map +1 -1
- package/dist/extension/background.js +91 -13
- package/dist/extension/background.js.map +1 -1
- package/dist/extension/flow.d.ts +36 -3
- package/dist/extension/flow.js +54 -19
- package/dist/extension/flow.js.map +1 -1
- package/dist/extension/inpage/builders.d.ts +103 -20
- package/dist/extension/inpage/builders.js +199 -52
- package/dist/extension/inpage/builders.js.map +1 -1
- package/dist/extension/inpage/observer.d.ts +36 -7
- package/dist/extension/inpage/observer.js +242 -16
- package/dist/extension/inpage/observer.js.map +1 -1
- package/dist/extension/manifest.json +1 -1
- package/dist/map/queries.d.ts +2 -0
- package/dist/map/queries.js +23 -1
- package/dist/map/queries.js.map +1 -1
- package/dist/map/tools.js +26 -4
- package/dist/map/tools.js.map +1 -1
- package/dist/tools/browser-definitions.js +80 -18
- package/dist/tools/browser-definitions.js.map +1 -1
- package/dist/tools/browser-dispatch.d.ts +19 -26
- package/dist/tools/browser-dispatch.js +152 -14
- package/dist/tools/browser-dispatch.js.map +1 -1
- package/dist/tools/server-instructions.js +14 -3
- package/dist/tools/server-instructions.js.map +1 -1
- package/package.json +1 -1
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* `cdp/builders-parity.test.ts` checks the built output.
|
|
12
12
|
*/
|
|
13
13
|
/** Bump whenever the in-page protocol (global shape, messages, methods) changes. */
|
|
14
|
-
export const OBSERVER_VERSION =
|
|
14
|
+
export const OBSERVER_VERSION = 3;
|
|
15
15
|
/** Name of the `Runtime.addBinding` function the observer pushes messages through. */
|
|
16
16
|
export const OBSERVER_BINDING = '__blPageObserverPush';
|
|
17
17
|
/** Well-known global that holds the installed observer instance. */
|
|
@@ -32,10 +32,30 @@ function clampInt(value, fallback, min, max) {
|
|
|
32
32
|
* `{ ok: true, installed, version, doc_id, coverage }`.
|
|
33
33
|
*
|
|
34
34
|
* The instance also owns element identity (spec v4 §5.6): `refFor(el)` issues
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
35
|
+
* `<doc_tag>:e<N>` (`doc_tag` = the first 6 characters of `doc_id`, N
|
|
36
|
+
* monotonic per document instance, from 1) the first time an element is seen
|
|
37
|
+
* and reuses it while the node lives; `resolveRef(ref)` returns the element
|
|
38
|
+
* while it is connected, else `null`. Refs are only valid for the document
|
|
39
|
+
* instance that issued them: a ref whose tag is not this instance's tag, or a
|
|
40
|
+
* bare `e<N>` with no tag, is `null` everywhere (resolve, re-resolve, key)
|
|
41
|
+
* without consulting the registries. Neither method throws.
|
|
42
|
+
*
|
|
43
|
+
* Secondary identity (spec v4 §5.6, decided by lab 10): `semanticKeyFor(el)`
|
|
44
|
+
* returns `{ role, name, path }` — role from the `role` attribute or a small
|
|
45
|
+
* implicit table, accessible-ish name (aria-label, aria-labelledby, associated
|
|
46
|
+
* label, text ≤ 80 chars, placeholder, title, button-like input value) and up
|
|
47
|
+
* to 8 `{ tag, index }` ancestor steps (1-based same-tag sibling index, a
|
|
48
|
+
* shadow boundary as `{ tag: '#shadow', index: 0 }`). `refFor` stores the key
|
|
49
|
+
* when it issues a ref. `resolveRefDetailed(ref)` returns `{ el, reresolved }`:
|
|
50
|
+
* the live node with `reresolved: false`, else the element with the same role,
|
|
51
|
+
* name and tag-only path when exactly one exists, rebinding the ref to it
|
|
52
|
+
* (`reresolved: true`); zero or several candidates give `null` (sibling
|
|
53
|
+
* indices are kept in the key as data but never pick among candidates). Candidates come from the document, every open shadow root reachable
|
|
54
|
+
* from it, and every root the `attachShadow` patch observed (closed ones
|
|
55
|
+
* included); closed roots attached before install are not reachable.
|
|
56
|
+
* `resolveRef(ref)` is `resolveRefDetailed(ref)?.el ?? null`. `keyForRef(ref)`
|
|
57
|
+
* returns a copy of the stored key or `null`. None of these throws.
|
|
58
|
+
* `nextSnapshotId()`
|
|
39
59
|
* issues `<doc_id>:s<N>` (N monotonic per document instance, from 1) for each
|
|
40
60
|
* `snapshot` result and returns `null` once the instance is torn down; the
|
|
41
61
|
* `doc_id` prefix lets a stale id from another document be rejected.
|
|
@@ -86,7 +106,14 @@ export function buildObserverInstallJs(opts = {}) {
|
|
|
86
106
|
const observed = new WeakSet();
|
|
87
107
|
let refs = new WeakMap();
|
|
88
108
|
const refNodes = new Map();
|
|
109
|
+
// Semantic key captured when each ref is issued; kept while the ref lives so
|
|
110
|
+
// a replaced node can be found again (spec v4 §5.6 secondary identity).
|
|
111
|
+
const refKeys = new Map();
|
|
89
112
|
let refSeq = 0;
|
|
113
|
+
// Every ref carries the issuing document's tag, so a ref from another
|
|
114
|
+
// document instance (or a bare e<N>) never reaches the registries.
|
|
115
|
+
const refPrefix = g.doc_id.slice(0, 6) + ':e';
|
|
116
|
+
const ownsRef = (ref) => typeof ref === 'string' && ref.startsWith(refPrefix);
|
|
90
117
|
let snapshotSeq = 0;
|
|
91
118
|
// Last SNAPSHOT_KEEP snapshot records (opaque to the observer; the snapshot
|
|
92
119
|
// builder owns their shape), keyed by snapshot_id in insertion order.
|
|
@@ -214,7 +241,7 @@ export function buildObserverInstallJs(opts = {}) {
|
|
|
214
241
|
try {
|
|
215
242
|
const mo = new MutationObserver(onRecords(kind));
|
|
216
243
|
mo.observe(root, { subtree: true, childList: true, attributes: true, characterData: true });
|
|
217
|
-
observers.push({ mo: mo, kind: kind });
|
|
244
|
+
observers.push({ mo: mo, kind: kind, root: root });
|
|
218
245
|
g.roots++;
|
|
219
246
|
} catch (e) {}
|
|
220
247
|
}
|
|
@@ -282,13 +309,179 @@ export function buildObserverInstallJs(opts = {}) {
|
|
|
282
309
|
|
|
283
310
|
g.flush = flush;
|
|
284
311
|
|
|
312
|
+
const KEY_PATH_STEPS = 8;
|
|
313
|
+
const KEY_TEXT_CAP = 80;
|
|
314
|
+
// Elements an HTML <label> can be associated with.
|
|
315
|
+
const LABELABLE = ['button', 'input', 'meter', 'output', 'progress', 'select', 'textarea'];
|
|
316
|
+
const BUTTON_INPUT_TYPES = ['button', 'submit', 'reset'];
|
|
317
|
+
|
|
318
|
+
function tagOf(el) {
|
|
319
|
+
return el.localName || String(el.nodeName).toLowerCase();
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function collapse(text) {
|
|
323
|
+
return String(text || '').replace(/\\s+/g, ' ').trim();
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function inputType(el) {
|
|
327
|
+
return (el.getAttribute('type') || 'text').toLowerCase();
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function roleOf(el) {
|
|
331
|
+
const explicit = (el.getAttribute('role') || '').trim();
|
|
332
|
+
if (explicit) return explicit;
|
|
333
|
+
const tag = tagOf(el);
|
|
334
|
+
if (tag === 'a' && el.hasAttribute('href')) return 'link';
|
|
335
|
+
if (tag === 'button' || tag === 'summary') return 'button';
|
|
336
|
+
if (tag === 'input') {
|
|
337
|
+
const type = inputType(el);
|
|
338
|
+
if (type === 'checkbox' || type === 'radio') return type;
|
|
339
|
+
if (BUTTON_INPUT_TYPES.indexOf(type) >= 0) return 'button';
|
|
340
|
+
return 'textbox';
|
|
341
|
+
}
|
|
342
|
+
if (tag === 'textarea') return 'textbox';
|
|
343
|
+
if (tag === 'select') return 'combobox';
|
|
344
|
+
if (el.hasAttribute('contenteditable') && el.getAttribute('contenteditable') !== 'false') return 'textbox';
|
|
345
|
+
return tag;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function labelTextOf(el, root) {
|
|
349
|
+
if (LABELABLE.indexOf(tagOf(el)) < 0) return '';
|
|
350
|
+
const id = el.getAttribute('id');
|
|
351
|
+
if (id && root && typeof root.querySelectorAll === 'function') {
|
|
352
|
+
for (const label of root.querySelectorAll('label')) {
|
|
353
|
+
if (label.getAttribute('for') !== id) continue;
|
|
354
|
+
const text = collapse(label.textContent);
|
|
355
|
+
if (text) return text;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
const wrapping = typeof el.closest === 'function' ? el.closest('label') : null;
|
|
359
|
+
return wrapping ? collapse(wrapping.textContent) : '';
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function nameOf(el) {
|
|
363
|
+
const ariaLabel = collapse(el.getAttribute('aria-label'));
|
|
364
|
+
if (ariaLabel) return ariaLabel;
|
|
365
|
+
const root = typeof el.getRootNode === 'function' ? el.getRootNode() : document;
|
|
366
|
+
const labelledBy = collapse(el.getAttribute('aria-labelledby'));
|
|
367
|
+
if (labelledBy && root && typeof root.getElementById === 'function') {
|
|
368
|
+
const parts = [];
|
|
369
|
+
for (const id of labelledBy.split(' ')) {
|
|
370
|
+
const target = root.getElementById(id);
|
|
371
|
+
const text = target ? collapse(target.textContent) : '';
|
|
372
|
+
if (text) parts.push(text);
|
|
373
|
+
}
|
|
374
|
+
if (parts.length > 0) return parts.join(' ');
|
|
375
|
+
}
|
|
376
|
+
const label = labelTextOf(el, root);
|
|
377
|
+
if (label) return label;
|
|
378
|
+
const text = collapse(el.textContent).slice(0, KEY_TEXT_CAP);
|
|
379
|
+
if (text) return text;
|
|
380
|
+
const placeholder = collapse(el.getAttribute('placeholder'));
|
|
381
|
+
if (placeholder) return placeholder;
|
|
382
|
+
const title = collapse(el.getAttribute('title'));
|
|
383
|
+
if (title) return title;
|
|
384
|
+
if (tagOf(el) === 'input' && BUTTON_INPUT_TYPES.indexOf(inputType(el)) >= 0) return collapse(el.value);
|
|
385
|
+
return '';
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function sameTagIndex(el) {
|
|
389
|
+
const tag = el.localName;
|
|
390
|
+
let index = 1;
|
|
391
|
+
for (let s = el.previousElementSibling; s; s = s.previousElementSibling) {
|
|
392
|
+
if (s.localName === tag) index++;
|
|
393
|
+
}
|
|
394
|
+
return index;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Unlike pathOf (descriptors, pinned settle output), this path carries the
|
|
398
|
+
// same-tag sibling index: role + name alone collides (spec v4 §4.6).
|
|
399
|
+
function keyPathOf(el) {
|
|
400
|
+
const steps = [];
|
|
401
|
+
let n = el;
|
|
402
|
+
while (n && steps.length < KEY_PATH_STEPS) {
|
|
403
|
+
if (n.nodeType === 1) {
|
|
404
|
+
if (n === document.documentElement) break;
|
|
405
|
+
steps.unshift({ tag: tagOf(n), index: sameTagIndex(n) });
|
|
406
|
+
n = n.parentNode;
|
|
407
|
+
} else if (n.nodeType === 11 && n.host) {
|
|
408
|
+
steps.unshift({ tag: '#shadow', index: 0 });
|
|
409
|
+
n = n.host;
|
|
410
|
+
} else {
|
|
411
|
+
break;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return steps;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function semanticKeyFor(el) {
|
|
418
|
+
try {
|
|
419
|
+
if (!el || el.nodeType !== 1) return null;
|
|
420
|
+
return { role: roleOf(el), name: nameOf(el), path: keyPathOf(el) };
|
|
421
|
+
} catch (e) {
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// The document, every open shadow root reachable from it, and every root the
|
|
427
|
+
// attachShadow patch observed (closed ones included) whose host is connected.
|
|
428
|
+
function searchRoots() {
|
|
429
|
+
const roots = [document];
|
|
430
|
+
const seen = new Set(roots);
|
|
431
|
+
const add = (root) => {
|
|
432
|
+
if (!root || seen.has(root)) return;
|
|
433
|
+
if (root.nodeType === 11 && !(root.host && root.host.isConnected)) return;
|
|
434
|
+
seen.add(root);
|
|
435
|
+
roots.push(root);
|
|
436
|
+
};
|
|
437
|
+
for (const o of observers) add(o.root);
|
|
438
|
+
for (let i = 0; i < roots.length; i++) {
|
|
439
|
+
let all;
|
|
440
|
+
try { all = roots[i].querySelectorAll('*'); } catch (e) { continue; }
|
|
441
|
+
for (const el of all) {
|
|
442
|
+
if (el.shadowRoot) add(el.shadowRoot);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
return roots;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Same role and name and an identical tag-only path, and exactly one such
|
|
449
|
+
// element: identical rows are refused, never guessed, so a ref never jumps
|
|
450
|
+
// across rows. Sibling indices stay in the key but select nothing.
|
|
451
|
+
function findByKey(key) {
|
|
452
|
+
const last = key.path[key.path.length - 1];
|
|
453
|
+
if (!last) return null;
|
|
454
|
+
let found = null;
|
|
455
|
+
for (const root of searchRoots()) {
|
|
456
|
+
let all;
|
|
457
|
+
try { all = root.querySelectorAll('*'); } catch (e) { continue; }
|
|
458
|
+
for (const el of all) {
|
|
459
|
+
if (tagOf(el) !== last.tag || roleOf(el) !== key.role || nameOf(el) !== key.name) continue;
|
|
460
|
+
const path = keyPathOf(el);
|
|
461
|
+
if (path.length !== key.path.length) continue;
|
|
462
|
+
let same = true;
|
|
463
|
+
for (let i = 0; i < path.length && same; i++) {
|
|
464
|
+
if (path[i].tag !== key.path[i].tag) same = false;
|
|
465
|
+
}
|
|
466
|
+
if (!same) continue;
|
|
467
|
+
if (found) return null;
|
|
468
|
+
found = el;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
return found;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
g.semanticKeyFor = semanticKeyFor;
|
|
475
|
+
|
|
285
476
|
g.refFor = function (el) {
|
|
286
477
|
try {
|
|
287
478
|
if (g.dead || !el || el.nodeType !== 1) return null;
|
|
288
479
|
let ref = refs.get(el);
|
|
289
480
|
if (!ref) {
|
|
290
|
-
ref =
|
|
481
|
+
ref = refPrefix + ++refSeq;
|
|
291
482
|
refs.set(el, ref);
|
|
483
|
+
const key = semanticKeyFor(el);
|
|
484
|
+
if (key) refKeys.set(ref, key);
|
|
292
485
|
}
|
|
293
486
|
// Same node, same ref: re-register one pruned while it was detached.
|
|
294
487
|
if (!refNodes.has(ref)) refNodes.set(ref, holdWeakly(el));
|
|
@@ -298,20 +491,40 @@ export function buildObserverInstallJs(opts = {}) {
|
|
|
298
491
|
}
|
|
299
492
|
};
|
|
300
493
|
|
|
301
|
-
g.
|
|
494
|
+
g.resolveRefDetailed = function (ref) {
|
|
302
495
|
try {
|
|
303
|
-
if (
|
|
496
|
+
if (g.dead || !ownsRef(ref)) return null;
|
|
304
497
|
const held = refNodes.get(ref);
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
if (el && el.isConnected) return el;
|
|
498
|
+
const old = held ? held.deref() : null;
|
|
499
|
+
if (old && old.isConnected) return { el: old, reresolved: false };
|
|
308
500
|
refNodes.delete(ref);
|
|
309
|
-
|
|
501
|
+
const key = refKeys.get(ref);
|
|
502
|
+
if (!key) return null;
|
|
503
|
+
const el = findByKey(key);
|
|
504
|
+
if (!el) return null;
|
|
505
|
+
if (old && refs.get(old) === ref) refs.delete(old);
|
|
506
|
+
// A node that already has its own ref keeps it for refFor; this ref
|
|
507
|
+
// still resolves to it through refNodes.
|
|
508
|
+
if (!refs.has(el)) refs.set(el, ref);
|
|
509
|
+
refNodes.set(ref, holdWeakly(el));
|
|
510
|
+
return { el: el, reresolved: true };
|
|
310
511
|
} catch (e) {
|
|
311
512
|
return null;
|
|
312
513
|
}
|
|
313
514
|
};
|
|
314
515
|
|
|
516
|
+
g.resolveRef = function (ref) {
|
|
517
|
+
const result = g.resolveRefDetailed(ref);
|
|
518
|
+
return result ? result.el : null;
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
g.keyForRef = function (ref) {
|
|
522
|
+
if (g.dead || !ownsRef(ref)) return null;
|
|
523
|
+
const key = refKeys.get(ref);
|
|
524
|
+
if (!key) return null;
|
|
525
|
+
return { role: key.role, name: key.name, path: key.path.map((s) => ({ tag: s.tag, index: s.index })) };
|
|
526
|
+
};
|
|
527
|
+
|
|
315
528
|
g.nextSnapshotId = function () {
|
|
316
529
|
if (g.dead) return null;
|
|
317
530
|
return g.doc_id + ':s' + ++snapshotSeq;
|
|
@@ -411,6 +624,7 @@ export function buildObserverInstallJs(opts = {}) {
|
|
|
411
624
|
g.baselines.clear();
|
|
412
625
|
refs = new WeakMap();
|
|
413
626
|
refNodes.clear();
|
|
627
|
+
refKeys.clear();
|
|
414
628
|
snapshotSeq = 0;
|
|
415
629
|
snapshots.clear();
|
|
416
630
|
if (window[opts.key] === g) {
|
|
@@ -479,12 +693,24 @@ function jsStringLiteral(value) {
|
|
|
479
693
|
/**
|
|
480
694
|
* Build the ref-resolve expression, for tests and debugging. Evaluates to
|
|
481
695
|
* `{ ok: true, found: true, tag }` when `ref` names an element of the current
|
|
482
|
-
* document that is still connected
|
|
483
|
-
*
|
|
696
|
+
* document that is still connected — plus `reresolved: true` when the node was
|
|
697
|
+
* gone and the ref was just rebound by its semantic key —
|
|
698
|
+
* `{ ok: true, found: false }` when it is unknown or stale, or
|
|
699
|
+
* `{ ok: false, reason: 'observer-unavailable' }`.
|
|
484
700
|
*/
|
|
485
701
|
export function buildObserverRefResolveJs(ref) {
|
|
486
702
|
const refJson = jsStringLiteral(typeof ref === 'string' ? ref : '');
|
|
487
|
-
return callInstance(`((
|
|
703
|
+
return callInstance(`((r) => { if (!r) return { ok: true, found: false }; const tag = r.el.localName || String(r.el.nodeName).toLowerCase(); return r.reresolved ? { ok: true, found: true, tag: tag, reresolved: true } : { ok: true, found: true, tag: tag }; })(g.resolveRefDetailed(${refJson}))`);
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* Build the semantic-key read expression. Evaluates to `{ ok: true, key }`
|
|
707
|
+
* with the `{ role, name, path }` stored when `ref` was issued, `{ ok: true,
|
|
708
|
+
* key: null }` for a ref this document instance does not know, or
|
|
709
|
+
* `{ ok: false, reason: 'observer-unavailable' }`.
|
|
710
|
+
*/
|
|
711
|
+
export function buildObserverSemanticKeyJs(ref) {
|
|
712
|
+
const refJson = jsStringLiteral(typeof ref === 'string' ? ref : '');
|
|
713
|
+
return callInstance(`({ ok: true, key: g.keyForRef(${refJson}) })`);
|
|
488
714
|
}
|
|
489
715
|
/**
|
|
490
716
|
* Build the teardown expression: disconnects every observer, restores the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"observer.js","sourceRoot":"","sources":["../../../src/cdp/inpage/observer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,oFAAoF;AACpF,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAC;AAElC,sFAAsF;AACtF,MAAM,CAAC,MAAM,gBAAgB,GAAG,sBAAsB,CAAC;AAEvD,oEAAoE;AACpE,MAAM,CAAC,MAAM,eAAe,GAAG,kBAAkB,CAAC;AAElD,MAAM,sBAAsB,GAAG,EAAE,CAAC;AAClC,MAAM,kBAAkB,GAAG,GAAG,CAAC;AAC/B,MAAM,iBAAiB,GAAG,EAAE,CAAC;AAC7B,MAAM,aAAa,GAAG,IAAI,CAAC;AAa3B,SAAS,QAAQ,CAAC,KAAc,EAAE,QAAgB,EAAE,GAAW,EAAE,GAAW;IAC1E,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,QAAQ,CAAC;IAC1E,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AACzD,CAAC;AAED
|
|
1
|
+
{"version":3,"file":"observer.js","sourceRoot":"","sources":["../../../src/cdp/inpage/observer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,oFAAoF;AACpF,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAC;AAElC,sFAAsF;AACtF,MAAM,CAAC,MAAM,gBAAgB,GAAG,sBAAsB,CAAC;AAEvD,oEAAoE;AACpE,MAAM,CAAC,MAAM,eAAe,GAAG,kBAAkB,CAAC;AAElD,MAAM,sBAAsB,GAAG,EAAE,CAAC;AAClC,MAAM,kBAAkB,GAAG,GAAG,CAAC;AAC/B,MAAM,iBAAiB,GAAG,EAAE,CAAC;AAC7B,MAAM,aAAa,GAAG,IAAI,CAAC;AAa3B,SAAS,QAAQ,CAAC,KAAc,EAAE,QAAgB,EAAE,GAAW,EAAE,GAAW;IAC1E,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,QAAQ,CAAC;IAC1E,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AACzD,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,MAAM,UAAU,sBAAsB,CAAC,OAA4B,EAAE;IACnE,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC;QAC9B,GAAG,EAAE,eAAe;QACpB,OAAO,EACL,OAAO,IAAI,CAAC,OAAO,KAAK,QAAQ,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,gBAAgB;QAC/F,OAAO,EAAE,QAAQ,CAAC,IAAI,CAAC,OAAO,EAAE,gBAAgB,EAAE,CAAC,EAAE,MAAM,CAAC,gBAAgB,CAAC;QAC7E,GAAG,EAAE,QAAQ,CAAC,IAAI,CAAC,cAAc,EAAE,sBAAsB,EAAE,CAAC,EAAE,kBAAkB,CAAC;QACjF,QAAQ,EAAE,QAAQ,CAAC,IAAI,CAAC,SAAS,EAAE,iBAAiB,EAAE,CAAC,EAAE,aAAa,CAAC;KACxE,CAAC,CAAC;IACH,OAAO;;iBAEQ,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAijBpB,CAAC;AACN,CAAC;AAED,MAAM,WAAW,GAAG,+CAA+C,CAAC;AAEpE,SAAS,YAAY,CAAC,IAAY;IAChC,OAAO;;qBAEY,IAAI,CAAC,SAAS,CAAC,eAAe,CAAC;6DACS,WAAW;;aAE3D,IAAI;;aAEJ,WAAW;;KAEnB,CAAC;AACN,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB;IACjC,OAAO,YAAY,CAAC,UAAU,CAAC,CAAC;AAClC,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,sBAAsB,CAAC,IAAY,EAAE,KAAc;IACjE,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC,gBAAgB,CAAC,CAAC,CAAC;IAC/E,MAAM,OAAO,GAAG,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;IAChG,OAAO,YAAY,CAAC,aAAa,QAAQ,KAAK,OAAO,GAAG,CAAC,CAAC;AAC5D,CAAC;AAED;;;;;GAKG;AACH,SAAS,eAAe,CAAC,KAAa;IACpC,yEAAyE;IACzE,0EAA0E;IAC1E,MAAM,aAAa,GAAG,MAAM,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;IAClD,MAAM,kBAAkB,GAAG,MAAM,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;IACvD,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC;SACzB,KAAK,CAAC,aAAa,CAAC;SACpB,IAAI,CAAC,SAAS,CAAC;SACf,KAAK,CAAC,kBAAkB,CAAC;SACzB,IAAI,CAAC,SAAS,CAAC;SACf,OAAO,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;AAC9B,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,yBAAyB,CAAC,GAAW;IACnD,MAAM,OAAO,GAAG,eAAe,CAAC,OAAO,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IACpE,OAAO,YAAY,CACjB,sQAAsQ,OAAO,IAAI,CAClR,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,0BAA0B,CAAC,GAAW;IACpD,MAAM,OAAO,GAAG,eAAe,CAAC,OAAO,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IACpE,OAAO,YAAY,CAAC,iCAAiC,OAAO,MAAM,CAAC,CAAC;AACtE,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,uBAAuB;IACrC,OAAO;;gBAEO,IAAI,CAAC,SAAS,CAAC,eAAe,CAAC;;;;;;;;;;KAU1C,CAAC;AACN,CAAC"}
|
package/dist/cdp/transport.js
CHANGED
|
@@ -30,7 +30,7 @@ import { resolveSettleParams, settleSafely } from './settle.js';
|
|
|
30
30
|
import { beginExpect, resolveExpectParams, verifyExpect } from './expect.js';
|
|
31
31
|
import { buildKeyEventSequence, modifiersToBitmask, resolveKey } from './keymap.js';
|
|
32
32
|
import { cancelDetachedFlow, detachedFlowForTab, detachedFlowStatus, finishDetachedFlow, registerDetachedFlow, reportDetachedProgress, shouldStopDetachedFlow, } from './detached-flows.js';
|
|
33
|
-
import { runFlow, } from './flow.js';
|
|
33
|
+
import { refFailureMessage, runFlow, } from './flow.js';
|
|
34
34
|
// The declarative v1 support table lives in `./support.ts` (zero
|
|
35
35
|
// dependencies, importable by `tools/browser-dispatch.ts` without pulling
|
|
36
36
|
// in this whole transport) — re-exported here so existing callers of
|
|
@@ -375,6 +375,18 @@ async function dispatchKeyEventSequence(client, def, modifiers) {
|
|
|
375
375
|
}
|
|
376
376
|
async function performFind(client, params) {
|
|
377
377
|
try {
|
|
378
|
+
if (params.key !== undefined) {
|
|
379
|
+
const value = await evaluateInTab(client, buildFindJs({ key: params.key }));
|
|
380
|
+
return { ok: true, result: value };
|
|
381
|
+
}
|
|
382
|
+
if (params.ref !== undefined) {
|
|
383
|
+
const ref = params.ref;
|
|
384
|
+
const found = await evaluateInTab(client, buildFindJs({ ref }));
|
|
385
|
+
const refError = found.matched ? null : refFailureMessage(found, ref);
|
|
386
|
+
if (refError)
|
|
387
|
+
return { ok: false, error: refError };
|
|
388
|
+
return { ok: true, result: found };
|
|
389
|
+
}
|
|
378
390
|
if (!params.text)
|
|
379
391
|
return { ok: false, error: 'find: text required' };
|
|
380
392
|
const opts = { text: params.text, role: params.role, exact: params.exact === true };
|
|
@@ -387,16 +399,19 @@ async function performFind(client, params) {
|
|
|
387
399
|
}
|
|
388
400
|
async function performClick(client, params) {
|
|
389
401
|
try {
|
|
390
|
-
const { selector, force = false } = params;
|
|
402
|
+
const { selector, ref, force = false } = params;
|
|
391
403
|
const settle = resolveSettleParams({
|
|
392
404
|
settle_ms: params.settle_ms,
|
|
393
405
|
settle_timeout_ms: params.settle_timeout_ms,
|
|
394
406
|
});
|
|
395
407
|
const expectSpec = resolveExpectParams(params.expect);
|
|
396
408
|
const evaluate = (expression) => evaluateInTab(client, expression);
|
|
397
|
-
const resolveOpts = { selector, force };
|
|
409
|
+
const resolveOpts = ref !== undefined ? { ref, force } : { selector, force };
|
|
398
410
|
const resolved = await evaluateInTab(client, buildClickResolveJs(resolveOpts));
|
|
399
411
|
if (!resolved.ok) {
|
|
412
|
+
const refError = ref !== undefined ? refFailureMessage(resolved, ref) : null;
|
|
413
|
+
if (refError)
|
|
414
|
+
return { ok: false, error: refError };
|
|
400
415
|
if (resolved.reason === 'occluded') {
|
|
401
416
|
return {
|
|
402
417
|
ok: false,
|
|
@@ -406,7 +421,7 @@ async function performClick(client, params) {
|
|
|
406
421
|
if (resolved.reason === 'invalid-selector') {
|
|
407
422
|
return { ok: false, error: `Invalid selector "${selector}": ${resolved.error}` };
|
|
408
423
|
}
|
|
409
|
-
return { ok: false, error: `Element not found: ${selector}` };
|
|
424
|
+
return { ok: false, error: `Element not found: ${ref ?? selector}` };
|
|
410
425
|
}
|
|
411
426
|
const mark = await takeObserverMark(client, settle);
|
|
412
427
|
const baseline = await beginExpect(evaluate, expectSpec);
|
|
@@ -433,7 +448,11 @@ async function performClick(client, params) {
|
|
|
433
448
|
const verdict = await verifyExpect(evaluate, expectSpec, baseline, settleResult);
|
|
434
449
|
if (!verdict.ok)
|
|
435
450
|
return { ok: false, error: verdict.error };
|
|
436
|
-
const result = { clicked: selector, tag: resolved.tag };
|
|
451
|
+
const result = { clicked: ref ?? selector ?? '', tag: resolved.tag };
|
|
452
|
+
if (ref !== undefined)
|
|
453
|
+
result.ref = ref;
|
|
454
|
+
if (resolved.reresolved === true)
|
|
455
|
+
result.reresolved = true;
|
|
437
456
|
if (resolved.hit_element)
|
|
438
457
|
result.hit_element = resolved.hit_element;
|
|
439
458
|
if (settleResult)
|
|
@@ -448,14 +467,26 @@ async function performClick(client, params) {
|
|
|
448
467
|
}
|
|
449
468
|
async function performType(client, params) {
|
|
450
469
|
try {
|
|
451
|
-
const { selector, text, clear = false } = params;
|
|
470
|
+
const { selector, ref, text, clear = false } = params;
|
|
452
471
|
const settle = resolveSettleParams({
|
|
453
472
|
settle_ms: params.settle_ms,
|
|
454
473
|
settle_timeout_ms: params.settle_timeout_ms,
|
|
455
474
|
});
|
|
456
475
|
const expectSpec = resolveExpectParams(params.expect);
|
|
457
476
|
const evaluate = (expression) => evaluateInTab(client, expression);
|
|
458
|
-
|
|
477
|
+
let reresolved = false;
|
|
478
|
+
if (ref !== undefined) {
|
|
479
|
+
const opts = { ref, clear };
|
|
480
|
+
const resolved = await evaluateInTab(client, buildTypeResolveJs(opts));
|
|
481
|
+
if (!resolved.ok) {
|
|
482
|
+
return {
|
|
483
|
+
ok: false,
|
|
484
|
+
error: refFailureMessage(resolved, ref) ?? `Element not found: ${ref}`,
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
reresolved = resolved.reresolved === true;
|
|
488
|
+
}
|
|
489
|
+
else if (selector !== undefined) {
|
|
459
490
|
const opts = { selector, clear };
|
|
460
491
|
const resolved = await evaluateInTab(client, buildTypeResolveJs(opts));
|
|
461
492
|
if (!resolved.ok) {
|
|
@@ -475,6 +506,10 @@ async function performType(client, params) {
|
|
|
475
506
|
const result = { typed: text.length };
|
|
476
507
|
if (selector !== undefined)
|
|
477
508
|
result.selector = selector;
|
|
509
|
+
if (ref !== undefined)
|
|
510
|
+
result.ref = ref;
|
|
511
|
+
if (reresolved)
|
|
512
|
+
result.reresolved = true;
|
|
478
513
|
if (settleResult)
|
|
479
514
|
result.settle = settleResult;
|
|
480
515
|
if (verdict.expect)
|
|
@@ -501,7 +536,16 @@ async function performPress(client, params) {
|
|
|
501
536
|
});
|
|
502
537
|
const expectSpec = resolveExpectParams(params.expect);
|
|
503
538
|
const evaluate = (expression) => evaluateInTab(client, expression);
|
|
504
|
-
|
|
539
|
+
const ref = params.ref;
|
|
540
|
+
let reresolved = false;
|
|
541
|
+
if (ref !== undefined) {
|
|
542
|
+
const focused = await evaluateInTab(client, buildFocusJs({ ref }));
|
|
543
|
+
if (!focused.ok) {
|
|
544
|
+
return { ok: false, error: refFailureMessage(focused, ref) ?? `Element not found: ${ref}` };
|
|
545
|
+
}
|
|
546
|
+
reresolved = focused.reresolved === true;
|
|
547
|
+
}
|
|
548
|
+
else if (params.selector) {
|
|
505
549
|
const focused = await evaluateInTab(client, buildFocusJs({ selector: params.selector }));
|
|
506
550
|
if (!focused) {
|
|
507
551
|
return { ok: false, error: `Element not found: ${params.selector}` };
|
|
@@ -515,6 +559,10 @@ async function performPress(client, params) {
|
|
|
515
559
|
if (!verdict.ok)
|
|
516
560
|
return { ok: false, error: verdict.error };
|
|
517
561
|
const result = { key, modifiers: modifierNames };
|
|
562
|
+
if (ref !== undefined)
|
|
563
|
+
result.ref = ref;
|
|
564
|
+
if (reresolved)
|
|
565
|
+
result.reresolved = true;
|
|
518
566
|
if (settleResult)
|
|
519
567
|
result.settle = settleResult;
|
|
520
568
|
if (verdict.expect)
|
|
@@ -570,6 +618,10 @@ shouldCancel) {
|
|
|
570
618
|
? MAX_POLL_MS
|
|
571
619
|
: requestedPoll;
|
|
572
620
|
let check = null;
|
|
621
|
+
// Set when a ref probe reports that the page has no observer: the ref
|
|
622
|
+
// can never resolve, so the wait fails instead of timing out (or, for
|
|
623
|
+
// `detached` / `hidden`, matching a ref it never looked up).
|
|
624
|
+
const refProbe = { unavailable: false };
|
|
573
625
|
if (waitSelector !== undefined) {
|
|
574
626
|
const expr = buildWaitForSelectorJs(waitSelector, waitCondition);
|
|
575
627
|
check = async () => {
|
|
@@ -582,6 +634,22 @@ shouldCancel) {
|
|
|
582
634
|
}
|
|
583
635
|
};
|
|
584
636
|
}
|
|
637
|
+
else if (params.ref !== undefined) {
|
|
638
|
+
const expr = buildWaitForSelectorJs({ ref: params.ref }, waitCondition);
|
|
639
|
+
check = async () => {
|
|
640
|
+
try {
|
|
641
|
+
const value = await evaluateInTab(client, expr);
|
|
642
|
+
if (typeof value === 'object' && value !== null) {
|
|
643
|
+
refProbe.unavailable = true;
|
|
644
|
+
return true;
|
|
645
|
+
}
|
|
646
|
+
return value === true;
|
|
647
|
+
}
|
|
648
|
+
catch {
|
|
649
|
+
return false;
|
|
650
|
+
}
|
|
651
|
+
};
|
|
652
|
+
}
|
|
585
653
|
else if (waitExpression !== undefined) {
|
|
586
654
|
const wrapped = `Boolean(${waitExpression})`;
|
|
587
655
|
check = async () => {
|
|
@@ -613,6 +681,12 @@ shouldCancel) {
|
|
|
613
681
|
await sleep(pollIntervalMs);
|
|
614
682
|
}
|
|
615
683
|
const elapsedMs = Date.now() - startedAt;
|
|
684
|
+
if (refProbe.unavailable && params.ref !== undefined) {
|
|
685
|
+
return {
|
|
686
|
+
ok: false,
|
|
687
|
+
error: refFailureMessage({ reason: 'observer-unavailable' }, params.ref) ?? '',
|
|
688
|
+
};
|
|
689
|
+
}
|
|
616
690
|
// 'cancelled' is deliberately distinct from 'timeout': the step
|
|
617
691
|
// executor turns a timeout into a flow FAILURE (with a recovery
|
|
618
692
|
// snapshot) and a cancellation into a clean `stopped_by: 'cancelled'`.
|
|
@@ -739,14 +813,18 @@ export async function callCdpTool(tabId, tool, params, timeoutMs) {
|
|
|
739
813
|
text: typeof p.text === 'string' ? p.text : '',
|
|
740
814
|
role: typeof p.role === 'string' ? p.role : undefined,
|
|
741
815
|
exact: p.exact === true,
|
|
816
|
+
ref: typeof p.ref === 'string' ? p.ref : undefined,
|
|
817
|
+
key: typeof p.key === 'object' && p.key !== null ? p.key : undefined,
|
|
742
818
|
});
|
|
743
819
|
if (!outcome.ok)
|
|
744
820
|
throw new Error(outcome.error);
|
|
745
821
|
return outcome.result;
|
|
746
822
|
}
|
|
747
823
|
case 'click': {
|
|
824
|
+
const ref = typeof p.ref === 'string' ? p.ref : undefined;
|
|
748
825
|
const outcome = await performClick(client, {
|
|
749
|
-
selector: typeof p.selector === 'string' ? p.selector : '',
|
|
826
|
+
selector: typeof p.selector === 'string' ? p.selector : ref !== undefined ? undefined : '',
|
|
827
|
+
ref,
|
|
750
828
|
force: p.force === true,
|
|
751
829
|
settle_ms: typeof p.settle_ms === 'number' ? p.settle_ms : undefined,
|
|
752
830
|
settle_timeout_ms: typeof p.settle_timeout_ms === 'number' ? p.settle_timeout_ms : undefined,
|
|
@@ -759,6 +837,7 @@ export async function callCdpTool(tabId, tool, params, timeoutMs) {
|
|
|
759
837
|
case 'type': {
|
|
760
838
|
const outcome = await performType(client, {
|
|
761
839
|
selector: typeof p.selector === 'string' ? p.selector : undefined,
|
|
840
|
+
ref: typeof p.ref === 'string' ? p.ref : undefined,
|
|
762
841
|
text: typeof p.text === 'string' ? p.text : '',
|
|
763
842
|
clear: p.clear === true,
|
|
764
843
|
settle_ms: typeof p.settle_ms === 'number' ? p.settle_ms : undefined,
|
|
@@ -777,6 +856,7 @@ export async function callCdpTool(tabId, tool, params, timeoutMs) {
|
|
|
777
856
|
key: typeof p.key === 'string' ? p.key : undefined,
|
|
778
857
|
modifiers: modifierNames,
|
|
779
858
|
selector: typeof p.selector === 'string' ? p.selector : undefined,
|
|
859
|
+
ref: typeof p.ref === 'string' ? p.ref : undefined,
|
|
780
860
|
settle_ms: typeof p.settle_ms === 'number' ? p.settle_ms : undefined,
|
|
781
861
|
settle_timeout_ms: typeof p.settle_timeout_ms === 'number' ? p.settle_timeout_ms : undefined,
|
|
782
862
|
expect: p.expect,
|
|
@@ -792,6 +872,7 @@ export async function callCdpTool(tabId, tool, params, timeoutMs) {
|
|
|
792
872
|
case 'wait_for': {
|
|
793
873
|
const outcome = await performWaitFor(client, {
|
|
794
874
|
selector: typeof p.selector === 'string' ? p.selector : undefined,
|
|
875
|
+
ref: typeof p.ref === 'string' ? p.ref : undefined,
|
|
795
876
|
expression: typeof p.expression === 'string' ? p.expression : undefined,
|
|
796
877
|
network_url: typeof p.network_url === 'string' ? p.network_url : undefined,
|
|
797
878
|
condition: typeof p.condition === 'string' ? p.condition : undefined,
|