@jobshimo/browser-link 0.36.0 → 0.38.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 +5 -349
- package/dist/agent-instructions/content.js.map +1 -1
- package/dist/bridge/server.d.ts +5 -0
- package/dist/bridge/server.js +1 -1
- package/dist/bridge/server.js.map +1 -1
- package/dist/cdp/flow.d.ts +44 -3
- package/dist/cdp/flow.js +65 -19
- package/dist/cdp/flow.js.map +1 -1
- package/dist/cdp/inpage/builders.d.ts +110 -24
- package/dist/cdp/inpage/builders.js +203 -53
- package/dist/cdp/inpage/builders.js.map +1 -1
- package/dist/cdp/inpage/deep-query.d.ts +1 -1
- package/dist/cdp/inpage/deep-query.js +12 -9
- package/dist/cdp/inpage/deep-query.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 +96 -9
- package/dist/cdp/transport.js.map +1 -1
- package/dist/commands/about.js +4 -2
- package/dist/commands/about.js.map +1 -1
- package/dist/extension/background.js +97 -13
- package/dist/extension/background.js.map +1 -1
- package/dist/extension/flow.d.ts +44 -3
- package/dist/extension/flow.js +65 -19
- package/dist/extension/flow.js.map +1 -1
- package/dist/extension/inpage/builders.d.ts +110 -24
- package/dist/extension/inpage/builders.js +203 -53
- package/dist/extension/inpage/builders.js.map +1 -1
- package/dist/extension/inpage/deep-query.d.ts +1 -1
- package/dist/extension/inpage/deep-query.js +12 -9
- package/dist/extension/inpage/deep-query.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/manual/core.d.ts +20 -0
- package/dist/manual/core.js +71 -0
- package/dist/manual/core.js.map +1 -0
- package/dist/manual/render.d.ts +22 -0
- package/dist/manual/render.js +56 -0
- package/dist/manual/render.js.map +1 -0
- package/dist/manual/status.d.ts +23 -0
- package/dist/manual/status.js +52 -0
- package/dist/manual/status.js.map +1 -0
- package/dist/manual/tool.d.ts +20 -0
- package/dist/manual/tool.js +106 -0
- package/dist/manual/tool.js.map +1 -0
- package/dist/manual/topics.d.ts +18 -0
- package/dist/manual/topics.js +60 -0
- package/dist/manual/topics.js.map +1 -0
- 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/permissions.js +6 -0
- package/dist/permissions.js.map +1 -1
- package/dist/server.js +11 -14
- package/dist/server.js.map +1 -1
- package/dist/tools/browser-definitions.js +113 -23
- package/dist/tools/browser-definitions.js.map +1 -1
- package/dist/tools/browser-dispatch.d.ts +24 -26
- package/dist/tools/browser-dispatch.js +169 -14
- package/dist/tools/browser-dispatch.js.map +1 -1
- package/dist/tools/server-instructions.d.ts +24 -13
- package/dist/tools/server-instructions.js +25 -303
- 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":"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"}
|
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, retargetedClickMessage, 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,26 +399,35 @@ 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,
|
|
403
418
|
error: `Element covered by ${resolved.blocker} — click the covering element or dismiss it first`,
|
|
404
419
|
};
|
|
405
420
|
}
|
|
421
|
+
if (resolved.reason === 'retargeted') {
|
|
422
|
+
return {
|
|
423
|
+
ok: false,
|
|
424
|
+
error: retargetedClickMessage(resolved.hit_element, ref ?? selector ?? ''),
|
|
425
|
+
};
|
|
426
|
+
}
|
|
406
427
|
if (resolved.reason === 'invalid-selector') {
|
|
407
428
|
return { ok: false, error: `Invalid selector "${selector}": ${resolved.error}` };
|
|
408
429
|
}
|
|
409
|
-
return { ok: false, error: `Element not found: ${selector}` };
|
|
430
|
+
return { ok: false, error: `Element not found: ${ref ?? selector}` };
|
|
410
431
|
}
|
|
411
432
|
const mark = await takeObserverMark(client, settle);
|
|
412
433
|
const baseline = await beginExpect(evaluate, expectSpec);
|
|
@@ -433,7 +454,11 @@ async function performClick(client, params) {
|
|
|
433
454
|
const verdict = await verifyExpect(evaluate, expectSpec, baseline, settleResult);
|
|
434
455
|
if (!verdict.ok)
|
|
435
456
|
return { ok: false, error: verdict.error };
|
|
436
|
-
const result = { clicked: selector, tag: resolved.tag };
|
|
457
|
+
const result = { clicked: ref ?? selector ?? '', tag: resolved.tag };
|
|
458
|
+
if (ref !== undefined)
|
|
459
|
+
result.ref = ref;
|
|
460
|
+
if (resolved.reresolved === true)
|
|
461
|
+
result.reresolved = true;
|
|
437
462
|
if (resolved.hit_element)
|
|
438
463
|
result.hit_element = resolved.hit_element;
|
|
439
464
|
if (settleResult)
|
|
@@ -448,14 +473,26 @@ async function performClick(client, params) {
|
|
|
448
473
|
}
|
|
449
474
|
async function performType(client, params) {
|
|
450
475
|
try {
|
|
451
|
-
const { selector, text, clear = false } = params;
|
|
476
|
+
const { selector, ref, text, clear = false } = params;
|
|
452
477
|
const settle = resolveSettleParams({
|
|
453
478
|
settle_ms: params.settle_ms,
|
|
454
479
|
settle_timeout_ms: params.settle_timeout_ms,
|
|
455
480
|
});
|
|
456
481
|
const expectSpec = resolveExpectParams(params.expect);
|
|
457
482
|
const evaluate = (expression) => evaluateInTab(client, expression);
|
|
458
|
-
|
|
483
|
+
let reresolved = false;
|
|
484
|
+
if (ref !== undefined) {
|
|
485
|
+
const opts = { ref, clear };
|
|
486
|
+
const resolved = await evaluateInTab(client, buildTypeResolveJs(opts));
|
|
487
|
+
if (!resolved.ok) {
|
|
488
|
+
return {
|
|
489
|
+
ok: false,
|
|
490
|
+
error: refFailureMessage(resolved, ref) ?? `Element not found: ${ref}`,
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
reresolved = resolved.reresolved === true;
|
|
494
|
+
}
|
|
495
|
+
else if (selector !== undefined) {
|
|
459
496
|
const opts = { selector, clear };
|
|
460
497
|
const resolved = await evaluateInTab(client, buildTypeResolveJs(opts));
|
|
461
498
|
if (!resolved.ok) {
|
|
@@ -475,6 +512,10 @@ async function performType(client, params) {
|
|
|
475
512
|
const result = { typed: text.length };
|
|
476
513
|
if (selector !== undefined)
|
|
477
514
|
result.selector = selector;
|
|
515
|
+
if (ref !== undefined)
|
|
516
|
+
result.ref = ref;
|
|
517
|
+
if (reresolved)
|
|
518
|
+
result.reresolved = true;
|
|
478
519
|
if (settleResult)
|
|
479
520
|
result.settle = settleResult;
|
|
480
521
|
if (verdict.expect)
|
|
@@ -501,7 +542,16 @@ async function performPress(client, params) {
|
|
|
501
542
|
});
|
|
502
543
|
const expectSpec = resolveExpectParams(params.expect);
|
|
503
544
|
const evaluate = (expression) => evaluateInTab(client, expression);
|
|
504
|
-
|
|
545
|
+
const ref = params.ref;
|
|
546
|
+
let reresolved = false;
|
|
547
|
+
if (ref !== undefined) {
|
|
548
|
+
const focused = await evaluateInTab(client, buildFocusJs({ ref }));
|
|
549
|
+
if (!focused.ok) {
|
|
550
|
+
return { ok: false, error: refFailureMessage(focused, ref) ?? `Element not found: ${ref}` };
|
|
551
|
+
}
|
|
552
|
+
reresolved = focused.reresolved === true;
|
|
553
|
+
}
|
|
554
|
+
else if (params.selector) {
|
|
505
555
|
const focused = await evaluateInTab(client, buildFocusJs({ selector: params.selector }));
|
|
506
556
|
if (!focused) {
|
|
507
557
|
return { ok: false, error: `Element not found: ${params.selector}` };
|
|
@@ -515,6 +565,10 @@ async function performPress(client, params) {
|
|
|
515
565
|
if (!verdict.ok)
|
|
516
566
|
return { ok: false, error: verdict.error };
|
|
517
567
|
const result = { key, modifiers: modifierNames };
|
|
568
|
+
if (ref !== undefined)
|
|
569
|
+
result.ref = ref;
|
|
570
|
+
if (reresolved)
|
|
571
|
+
result.reresolved = true;
|
|
518
572
|
if (settleResult)
|
|
519
573
|
result.settle = settleResult;
|
|
520
574
|
if (verdict.expect)
|
|
@@ -570,6 +624,10 @@ shouldCancel) {
|
|
|
570
624
|
? MAX_POLL_MS
|
|
571
625
|
: requestedPoll;
|
|
572
626
|
let check = null;
|
|
627
|
+
// Set when a ref probe reports that the page has no observer: the ref
|
|
628
|
+
// can never resolve, so the wait fails instead of timing out (or, for
|
|
629
|
+
// `detached` / `hidden`, matching a ref it never looked up).
|
|
630
|
+
const refProbe = { unavailable: false };
|
|
573
631
|
if (waitSelector !== undefined) {
|
|
574
632
|
const expr = buildWaitForSelectorJs(waitSelector, waitCondition);
|
|
575
633
|
check = async () => {
|
|
@@ -582,6 +640,22 @@ shouldCancel) {
|
|
|
582
640
|
}
|
|
583
641
|
};
|
|
584
642
|
}
|
|
643
|
+
else if (params.ref !== undefined) {
|
|
644
|
+
const expr = buildWaitForSelectorJs({ ref: params.ref }, waitCondition);
|
|
645
|
+
check = async () => {
|
|
646
|
+
try {
|
|
647
|
+
const value = await evaluateInTab(client, expr);
|
|
648
|
+
if (typeof value === 'object' && value !== null) {
|
|
649
|
+
refProbe.unavailable = true;
|
|
650
|
+
return true;
|
|
651
|
+
}
|
|
652
|
+
return value === true;
|
|
653
|
+
}
|
|
654
|
+
catch {
|
|
655
|
+
return false;
|
|
656
|
+
}
|
|
657
|
+
};
|
|
658
|
+
}
|
|
585
659
|
else if (waitExpression !== undefined) {
|
|
586
660
|
const wrapped = `Boolean(${waitExpression})`;
|
|
587
661
|
check = async () => {
|
|
@@ -613,6 +687,12 @@ shouldCancel) {
|
|
|
613
687
|
await sleep(pollIntervalMs);
|
|
614
688
|
}
|
|
615
689
|
const elapsedMs = Date.now() - startedAt;
|
|
690
|
+
if (refProbe.unavailable && params.ref !== undefined) {
|
|
691
|
+
return {
|
|
692
|
+
ok: false,
|
|
693
|
+
error: refFailureMessage({ reason: 'observer-unavailable' }, params.ref) ?? '',
|
|
694
|
+
};
|
|
695
|
+
}
|
|
616
696
|
// 'cancelled' is deliberately distinct from 'timeout': the step
|
|
617
697
|
// executor turns a timeout into a flow FAILURE (with a recovery
|
|
618
698
|
// snapshot) and a cancellation into a clean `stopped_by: 'cancelled'`.
|
|
@@ -739,14 +819,18 @@ export async function callCdpTool(tabId, tool, params, timeoutMs) {
|
|
|
739
819
|
text: typeof p.text === 'string' ? p.text : '',
|
|
740
820
|
role: typeof p.role === 'string' ? p.role : undefined,
|
|
741
821
|
exact: p.exact === true,
|
|
822
|
+
ref: typeof p.ref === 'string' ? p.ref : undefined,
|
|
823
|
+
key: typeof p.key === 'object' && p.key !== null ? p.key : undefined,
|
|
742
824
|
});
|
|
743
825
|
if (!outcome.ok)
|
|
744
826
|
throw new Error(outcome.error);
|
|
745
827
|
return outcome.result;
|
|
746
828
|
}
|
|
747
829
|
case 'click': {
|
|
830
|
+
const ref = typeof p.ref === 'string' ? p.ref : undefined;
|
|
748
831
|
const outcome = await performClick(client, {
|
|
749
|
-
selector: typeof p.selector === 'string' ? p.selector : '',
|
|
832
|
+
selector: typeof p.selector === 'string' ? p.selector : ref !== undefined ? undefined : '',
|
|
833
|
+
ref,
|
|
750
834
|
force: p.force === true,
|
|
751
835
|
settle_ms: typeof p.settle_ms === 'number' ? p.settle_ms : undefined,
|
|
752
836
|
settle_timeout_ms: typeof p.settle_timeout_ms === 'number' ? p.settle_timeout_ms : undefined,
|
|
@@ -759,6 +843,7 @@ export async function callCdpTool(tabId, tool, params, timeoutMs) {
|
|
|
759
843
|
case 'type': {
|
|
760
844
|
const outcome = await performType(client, {
|
|
761
845
|
selector: typeof p.selector === 'string' ? p.selector : undefined,
|
|
846
|
+
ref: typeof p.ref === 'string' ? p.ref : undefined,
|
|
762
847
|
text: typeof p.text === 'string' ? p.text : '',
|
|
763
848
|
clear: p.clear === true,
|
|
764
849
|
settle_ms: typeof p.settle_ms === 'number' ? p.settle_ms : undefined,
|
|
@@ -777,6 +862,7 @@ export async function callCdpTool(tabId, tool, params, timeoutMs) {
|
|
|
777
862
|
key: typeof p.key === 'string' ? p.key : undefined,
|
|
778
863
|
modifiers: modifierNames,
|
|
779
864
|
selector: typeof p.selector === 'string' ? p.selector : undefined,
|
|
865
|
+
ref: typeof p.ref === 'string' ? p.ref : undefined,
|
|
780
866
|
settle_ms: typeof p.settle_ms === 'number' ? p.settle_ms : undefined,
|
|
781
867
|
settle_timeout_ms: typeof p.settle_timeout_ms === 'number' ? p.settle_timeout_ms : undefined,
|
|
782
868
|
expect: p.expect,
|
|
@@ -792,6 +878,7 @@ export async function callCdpTool(tabId, tool, params, timeoutMs) {
|
|
|
792
878
|
case 'wait_for': {
|
|
793
879
|
const outcome = await performWaitFor(client, {
|
|
794
880
|
selector: typeof p.selector === 'string' ? p.selector : undefined,
|
|
881
|
+
ref: typeof p.ref === 'string' ? p.ref : undefined,
|
|
795
882
|
expression: typeof p.expression === 'string' ? p.expression : undefined,
|
|
796
883
|
network_url: typeof p.network_url === 'string' ? p.network_url : undefined,
|
|
797
884
|
condition: typeof p.condition === 'string' ? p.condition : undefined,
|