@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.
Files changed (38) hide show
  1. package/dist/agent-instructions/content.js +14 -3
  2. package/dist/agent-instructions/content.js.map +1 -1
  3. package/dist/cdp/flow.d.ts +36 -3
  4. package/dist/cdp/flow.js +54 -19
  5. package/dist/cdp/flow.js.map +1 -1
  6. package/dist/cdp/inpage/builders.d.ts +103 -20
  7. package/dist/cdp/inpage/builders.js +199 -52
  8. package/dist/cdp/inpage/builders.js.map +1 -1
  9. package/dist/cdp/inpage/observer.d.ts +36 -7
  10. package/dist/cdp/inpage/observer.js +242 -16
  11. package/dist/cdp/inpage/observer.js.map +1 -1
  12. package/dist/cdp/transport.js +90 -9
  13. package/dist/cdp/transport.js.map +1 -1
  14. package/dist/extension/background.js +91 -13
  15. package/dist/extension/background.js.map +1 -1
  16. package/dist/extension/flow.d.ts +36 -3
  17. package/dist/extension/flow.js +54 -19
  18. package/dist/extension/flow.js.map +1 -1
  19. package/dist/extension/inpage/builders.d.ts +103 -20
  20. package/dist/extension/inpage/builders.js +199 -52
  21. package/dist/extension/inpage/builders.js.map +1 -1
  22. package/dist/extension/inpage/observer.d.ts +36 -7
  23. package/dist/extension/inpage/observer.js +242 -16
  24. package/dist/extension/inpage/observer.js.map +1 -1
  25. package/dist/extension/manifest.json +1 -1
  26. package/dist/map/queries.d.ts +2 -0
  27. package/dist/map/queries.js +23 -1
  28. package/dist/map/queries.js.map +1 -1
  29. package/dist/map/tools.js +26 -4
  30. package/dist/map/tools.js.map +1 -1
  31. package/dist/tools/browser-definitions.js +80 -18
  32. package/dist/tools/browser-definitions.js.map +1 -1
  33. package/dist/tools/browser-dispatch.d.ts +19 -26
  34. package/dist/tools/browser-dispatch.js +152 -14
  35. package/dist/tools/browser-dispatch.js.map +1 -1
  36. package/dist/tools/server-instructions.js +14 -3
  37. package/dist/tools/server-instructions.js.map +1 -1
  38. 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 = 2;
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
- * `e<N>` (monotonic per document instance, from `e1`) the first time an
36
- * element is seen and reuses it while the node lives; `resolveRef(ref)`
37
- * returns the element while it is connected, else `null`. Refs are only valid
38
- * for the `doc_id` that issued them. Neither method throws. `nextSnapshotId()`
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 = 'e' + ++refSeq;
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.resolveRef = function (ref) {
494
+ g.resolveRefDetailed = function (ref) {
302
495
  try {
303
- if (typeof ref !== 'string') return null;
496
+ if (g.dead || !ownsRef(ref)) return null;
304
497
  const held = refNodes.get(ref);
305
- if (!held) return null;
306
- const el = held.deref();
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
- return null;
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, `{ ok: true, found: false }` when it is
483
- * unknown or stale, or `{ ok: false, reason: 'observer-unavailable' }`.
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(`((el) => el ? { ok: true, found: true, tag: el.localName || String(el.nodeName).toLowerCase() } : { ok: true, found: false })(g.resolveRef(${refJson}))`);
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":"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"}
1
+ {"version":3,"file":"observer.js","sourceRoot":"","sources":["../../../src/cdp/inpage/observer.ts"],"names":[],"mappings":"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"}
@@ -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
- if (selector !== undefined) {
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
- if (params.selector) {
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,