@reticlehq/browser 3.6.0 → 3.7.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 (83) hide show
  1. package/dist/actions/action-report.d.ts +54 -0
  2. package/dist/actions/action-report.js +60 -0
  3. package/dist/actions/actions-dom.d.ts +1 -0
  4. package/dist/actions/actions-dom.js +12 -0
  5. package/dist/actions/actions-press.d.ts +1 -0
  6. package/dist/actions/actions-press.js +25 -1
  7. package/dist/actions/actions.d.ts +1 -37
  8. package/dist/actions/actions.js +24 -66
  9. package/dist/actions/appeared-text.js +3 -0
  10. package/dist/actions/danger-context.d.ts +2 -0
  11. package/dist/actions/danger-context.js +28 -2
  12. package/dist/actions/value-input.d.ts +1 -0
  13. package/dist/actions/value-input.js +11 -0
  14. package/dist/commands/commands.js +4 -0
  15. package/dist/dom/a11y.js +251 -10
  16. package/dist/dom/addressing/coverage-markers.d.ts +6 -0
  17. package/dist/dom/addressing/coverage-markers.js +13 -0
  18. package/dist/dom/dom-ignore.d.ts +2 -0
  19. package/dist/dom/dom-ignore.js +15 -2
  20. package/dist/dom/query.d.ts +1 -1
  21. package/dist/dom/query.js +78 -16
  22. package/dist/dom/render-page.d.ts +12 -0
  23. package/dist/dom/render-page.js +135 -0
  24. package/dist/dom/snapshot.js +55 -7
  25. package/dist/observers/dom.js +32 -16
  26. package/dist/observers/field.js +3 -21
  27. package/dist/observers/network.js +56 -7
  28. package/dist/observers/perf.js +53 -1
  29. package/dist/observers/route.js +6 -5
  30. package/dist/presenter/carousel/carousel.js +2 -7
  31. package/dist/presenter/carousel/panel-slides.d.ts +2 -4
  32. package/dist/presenter/carousel/panel-slides.js +17 -15
  33. package/dist/presenter/chrome/log-memory.d.ts +1 -0
  34. package/dist/presenter/chrome/presenter-log.d.ts +7 -0
  35. package/dist/presenter/chrome/presenter-log.js +48 -8
  36. package/dist/presenter/chrome/presenter-morph.d.ts +1 -0
  37. package/dist/presenter/chrome/presenter-morph.js +34 -0
  38. package/dist/presenter/chrome/presenter-report-copy.d.ts +17 -4
  39. package/dist/presenter/chrome/presenter-report-copy.js +26 -4
  40. package/dist/presenter/hud-telemetry.js +55 -4
  41. package/dist/presenter/presenter-account-styles.js +3 -3
  42. package/dist/presenter/presenter-account.d.ts +27 -5
  43. package/dist/presenter/presenter-account.js +83 -6
  44. package/dist/presenter/presenter-agent-link.d.ts +9 -0
  45. package/dist/presenter/presenter-agent-link.js +62 -0
  46. package/dist/presenter/presenter-chat-views.d.ts +27 -5
  47. package/dist/presenter/presenter-chat-views.js +312 -72
  48. package/dist/presenter/presenter-config.d.ts +2 -0
  49. package/dist/presenter/presenter-config.js +1 -0
  50. package/dist/presenter/presenter-controls.d.ts +0 -1
  51. package/dist/presenter/presenter-controls.js +13 -91
  52. package/dist/presenter/presenter-design-tokens.js +1 -1
  53. package/dist/presenter/presenter-frame.d.ts +1 -1
  54. package/dist/presenter/presenter-frame.js +8 -10
  55. package/dist/presenter/presenter-harness-row.d.ts +67 -0
  56. package/dist/presenter/presenter-harness-row.js +209 -0
  57. package/dist/presenter/presenter-personas.d.ts +35 -0
  58. package/dist/presenter/presenter-personas.js +95 -0
  59. package/dist/presenter/presenter-plan.js +25 -24
  60. package/dist/presenter/presenter-report-styles.js +40 -22
  61. package/dist/presenter/presenter-report.d.ts +2 -1
  62. package/dist/presenter/presenter-report.js +83 -17
  63. package/dist/presenter/presenter-settings-styles.js +8 -4
  64. package/dist/presenter/presenter-settings.d.ts +7 -6
  65. package/dist/presenter/presenter-settings.js +75 -34
  66. package/dist/presenter/presenter-shell-styles.js +12 -3
  67. package/dist/presenter/presenter-shell.d.ts +10 -4
  68. package/dist/presenter/presenter-shell.js +31 -23
  69. package/dist/presenter/presenter-styles.js +0 -2
  70. package/dist/presenter/presenter-workspace.js +10 -3
  71. package/dist/presenter/presenter.d.ts +3 -2
  72. package/dist/presenter/presenter.js +81 -20
  73. package/dist/registry/declared-platform.js +2 -0
  74. package/dist/reticle-inject.js +777 -700
  75. package/dist/reticle.js +36 -2
  76. package/dist/storage-keys.d.ts +3 -2
  77. package/dist/storage-keys.js +7 -3
  78. package/dist/transport/transport.js +5 -3
  79. package/dist/transport/unreachable-message.d.ts +1 -1
  80. package/dist/transport/unreachable-message.js +3 -5
  81. package/package.json +2 -2
  82. package/dist/presenter/carousel/offer-card.d.ts +0 -21
  83. package/dist/presenter/carousel/offer-card.js +0 -56
package/dist/dom/a11y.js CHANGED
@@ -277,6 +277,7 @@ export function getStates(el, visible = isVisible(el)) {
277
277
  states.push(ElementState.FOCUSED);
278
278
  return states;
279
279
  }
280
+ const SENSITIVE_AUTOCOMPLETE = /\b(cc-|current-password|new-password|one-time-code)/i;
280
281
  export function isSensitiveField(el) {
281
282
  if (!isInput(el) && !isTextArea(el) && !isSelect(el)) {
282
283
  return false;
@@ -289,7 +290,7 @@ export function isSensitiveField(el) {
289
290
  el.getAttribute(DEFAULT_TESTID_ATTR) ?? '',
290
291
  el.getAttribute('aria-label') ?? '',
291
292
  ];
292
- const sensitiveAutocomplete = /current-password|new-password|cc-number|cc-csc|one-time-code/i.test(autocomplete);
293
+ const sensitiveAutocomplete = SENSITIVE_AUTOCOMPLETE.test(autocomplete);
293
294
  return ((isInput(el) && 'password' === el.type.toLowerCase()) ||
294
295
  sensitiveAutocomplete ||
295
296
  identifiers.some(isSensitiveKey));
@@ -297,7 +298,7 @@ export function isSensitiveField(el) {
297
298
  export function getValue(el) {
298
299
  if (isInput(el) || isTextArea(el) || isSelect(el)) {
299
300
  if (isSensitiveField(el))
300
- return REDACTED_VALUE;
301
+ return '' === el.value ? '' : REDACTED_VALUE;
301
302
  return el.value;
302
303
  }
303
304
  const valueNow = el.getAttribute('aria-valuenow');
@@ -318,16 +319,18 @@ function stalledFade(el) {
318
319
  return false;
319
320
  return el.getAnimations().some((a) => 'running' === a.playState || a.pending);
320
321
  }
321
- function selfHidden(el) {
322
- if ('true' === el.getAttribute('aria-hidden'))
322
+ const OVERLAY_ARIA_HIDDEN = 'data-aria-hidden';
323
+ function hiddenByAnOverlay(el) {
324
+ return el.hasAttribute(OVERLAY_ARIA_HIDDEN);
325
+ }
326
+ function selfHidden(el, style) {
327
+ if ('true' === el.getAttribute('aria-hidden') && !hiddenByAnOverlay(el))
323
328
  return true;
324
329
  if (isHtmlElement(el) && el.hidden)
325
330
  return true;
326
331
  if (hiddenInsideClosedDetails(el))
327
332
  return true;
328
- const view = el.ownerDocument.defaultView;
329
- if (view !== null) {
330
- const style = view.getComputedStyle(el);
333
+ if (style !== null) {
331
334
  if ('none' === style.display ||
332
335
  'hidden' === style.visibility ||
333
336
  'collapse' === style.visibility) {
@@ -360,13 +363,252 @@ function parentAcrossShadowBoundary(el) {
360
363
  const host = el.getRootNode().host;
361
364
  return host ?? null;
362
365
  }
366
+ const CLIPPING_OVERFLOW = new Set(['hidden', 'clip']);
367
+ const SCROLLING_OVERFLOW = new Set(['auto', 'scroll']);
368
+ const NON_CLIPPING_DISPLAY = new Set([
369
+ 'contents',
370
+ 'table-row',
371
+ 'table-row-group',
372
+ 'table-header-group',
373
+ 'table-footer-group',
374
+ 'table-column',
375
+ 'table-column-group',
376
+ ]);
377
+ const TRANSFORM_CONTAINING_BLOCK_PROPERTIES = new Set([
378
+ 'transform',
379
+ 'translate',
380
+ 'rotate',
381
+ 'scale',
382
+ 'perspective',
383
+ ]);
384
+ const CONTAINING_BLOCK_PROPERTIES = new Set([
385
+ ...TRANSFORM_CONTAINING_BLOCK_PROPERTIES,
386
+ 'filter',
387
+ 'backdrop-filter',
388
+ ]);
389
+ const CONTAINING_BLOCK_CONTAIN = new Set(['layout', 'paint', 'strict', 'content']);
390
+ const REPLACED_INLINE_TAGS = new Set([
391
+ 'img',
392
+ 'video',
393
+ 'audio',
394
+ 'canvas',
395
+ 'iframe',
396
+ 'embed',
397
+ 'object',
398
+ 'input',
399
+ 'textarea',
400
+ 'select',
401
+ ]);
402
+ const visibilityInfoMemos = new WeakMap();
403
+ function visibilityInfoMemo(memo) {
404
+ if (memo === undefined)
405
+ return new Map();
406
+ let info = visibilityInfoMemos.get(memo);
407
+ if (info === undefined) {
408
+ info = new Map();
409
+ visibilityInfoMemos.set(memo, info);
410
+ }
411
+ return info;
412
+ }
413
+ function ownVisibilityInfo(el, memo) {
414
+ let info = memo.get(el);
415
+ if (info === undefined) {
416
+ const style = el.ownerDocument.defaultView?.getComputedStyle(el) ?? null;
417
+ if (selfHidden(el, style)) {
418
+ info = {
419
+ hidden: true,
420
+ style,
421
+ parent: null,
422
+ clipX: false,
423
+ clipY: false,
424
+ scrollX: false,
425
+ scrollY: false,
426
+ hasClipping: false,
427
+ };
428
+ memo.set(el, info);
429
+ return info;
430
+ }
431
+ const parent = parentAcrossShadowBoundary(el);
432
+ const inherited = null === parent ? undefined : ownVisibilityInfo(parent, memo);
433
+ const [overflowX, overflowY] = overflowAxes(style);
434
+ const root = el.ownerDocument.documentElement;
435
+ const viewportOverflow = el === root ||
436
+ (el === el.ownerDocument.body &&
437
+ overflowAxes(ownVisibilityInfo(root, memo).style).every((axis) => 'visible' === axis));
438
+ const hasClipBox = !viewportOverflow &&
439
+ !NON_CLIPPING_DISPLAY.has(style?.display ?? '') &&
440
+ !(isHtmlElement(el) && 'inline' === style?.display);
441
+ const clipX = hasClipBox && CLIPPING_OVERFLOW.has(overflowX);
442
+ const clipY = hasClipBox && CLIPPING_OVERFLOW.has(overflowY);
443
+ info = {
444
+ hidden: true === inherited?.hidden,
445
+ style,
446
+ parent,
447
+ clipX,
448
+ clipY,
449
+ scrollX: hasClipBox && SCROLLING_OVERFLOW.has(overflowX),
450
+ scrollY: hasClipBox && SCROLLING_OVERFLOW.has(overflowY),
451
+ hasClipping: clipX || clipY || true === inherited?.hasClipping,
452
+ };
453
+ memo.set(el, info);
454
+ }
455
+ return info;
456
+ }
457
+ function overflowAxes(style) {
458
+ const shorthand = style?.overflow ?? '';
459
+ const [x = 'visible', y = x] = ('' === shorthand ? 'visible' : shorthand).split(/\s+/);
460
+ return [
461
+ null === style || '' === style.overflowX ? x : style.overflowX,
462
+ null === style || '' === style.overflowY ? y : style.overflowY,
463
+ ];
464
+ }
465
+ function positionedContainingBlock(el, fixed, memo) {
466
+ for (let parent = ownVisibilityInfo(el, memo).parent; parent !== null; parent = ownVisibilityInfo(parent, memo).parent) {
467
+ const style = ownVisibilityInfo(parent, memo).style;
468
+ if (null === style || 'contents' === style.display)
469
+ continue;
470
+ if (!fixed && '' !== style.position && 'static' !== style.position)
471
+ return parent;
472
+ const transformable = !('inline' === style.display &&
473
+ isHtmlElement(parent) &&
474
+ !REPLACED_INLINE_TAGS.has(parent.localName)) &&
475
+ 'table-column' !== style.display &&
476
+ 'table-column-group' !== style.display;
477
+ const applies = (property) => transformable || !TRANSFORM_CONTAINING_BLOCK_PROPERTIES.has(property);
478
+ if ([...CONTAINING_BLOCK_PROPERTIES].some((property) => {
479
+ const value = style.getPropertyValue(property);
480
+ return applies(property) && '' !== value && 'none' !== value;
481
+ }) ||
482
+ style.willChange
483
+ .split(',')
484
+ .some((property) => CONTAINING_BLOCK_PROPERTIES.has(property.trim()) && applies(property.trim())) ||
485
+ (transformable &&
486
+ !NON_CLIPPING_DISPLAY.has(style.display) &&
487
+ (style.contain.split(/\s+/).some((value) => CONTAINING_BLOCK_CONTAIN.has(value)) ||
488
+ 'auto' === style.contentVisibility)))
489
+ return parent;
490
+ }
491
+ return null;
492
+ }
493
+ function clippingBox(el, info) {
494
+ if (info.clipBox !== undefined)
495
+ return info.clipBox;
496
+ const rect = el.getBoundingClientRect();
497
+ const style = info.style;
498
+ const px = (value) => Number.parseFloat(value ?? '') || 0;
499
+ const border = (value, kind) => kind === undefined || '' === kind || 'none' === kind || 'hidden' === kind ? 0 : px(value);
500
+ const scaleX = isHtmlElement(el) && el.offsetWidth > 0 ? rect.width / el.offsetWidth : 1;
501
+ const scaleY = isHtmlElement(el) && el.offsetHeight > 0 ? rect.height / el.offsetHeight : 1;
502
+ const left = isHtmlElement(el) && el.offsetWidth > 0
503
+ ? rect.left + el.clientLeft * scaleX
504
+ : rect.left + border(style?.borderLeftWidth, style?.borderLeftStyle);
505
+ const top = isHtmlElement(el) && el.offsetHeight > 0
506
+ ? rect.top + el.clientTop * scaleY
507
+ : rect.top + border(style?.borderTopWidth, style?.borderTopStyle);
508
+ const right = isHtmlElement(el) && el.offsetWidth > 0
509
+ ? left + el.clientWidth * scaleX
510
+ : rect.right - border(style?.borderRightWidth, style?.borderRightStyle);
511
+ const bottom = isHtmlElement(el) && el.offsetHeight > 0
512
+ ? top + el.clientHeight * scaleY
513
+ : rect.bottom - border(style?.borderBottomWidth, style?.borderBottomStyle);
514
+ const margin = (style?.overflowClipMargin ?? '').split(/\s+/);
515
+ const outset = px(margin.at(-1));
516
+ const borderBox = margin.includes('border-box');
517
+ const contentBox = margin.includes('content-box');
518
+ const [x, y] = overflowAxes(style);
519
+ const clipLeft = 'clip' === x
520
+ ? (borderBox ? rect.left : left + (contentBox ? px(style?.paddingLeft) * scaleX : 0)) -
521
+ outset * scaleX
522
+ : left;
523
+ const clipRight = 'clip' === x
524
+ ? (borderBox ? rect.right : right - (contentBox ? px(style?.paddingRight) * scaleX : 0)) +
525
+ outset * scaleX
526
+ : right;
527
+ const clipTop = 'clip' === y
528
+ ? (borderBox ? rect.top : top + (contentBox ? px(style?.paddingTop) * scaleY : 0)) -
529
+ outset * scaleY
530
+ : top;
531
+ const clipBottom = 'clip' === y
532
+ ? (borderBox ? rect.bottom : bottom - (contentBox ? px(style?.paddingBottom) * scaleY : 0)) +
533
+ outset * scaleY
534
+ : bottom;
535
+ info.clipBox = {
536
+ left: clipLeft,
537
+ right: clipRight,
538
+ top: clipTop,
539
+ bottom: clipBottom,
540
+ width: clipRight - clipLeft,
541
+ height: clipBottom - clipTop,
542
+ };
543
+ return info.clipBox;
544
+ }
545
+ function scrolledInto(b, port, alongX, alongY) {
546
+ const axis = (lo, hi, size, from, to) => {
547
+ if (hi > from && lo < to)
548
+ return [lo, hi];
549
+ const span = Math.min(size, to - from);
550
+ return lo >= to ? [to - span, to] : [from, from + span];
551
+ };
552
+ const [left, right] = alongX
553
+ ? axis(b.left, b.right, b.width, port.left, port.right)
554
+ : [b.left, b.right];
555
+ const [top, bottom] = alongY
556
+ ? axis(b.top, b.bottom, b.height, port.top, port.bottom)
557
+ : [b.top, b.bottom];
558
+ return { left, right, top, bottom, width: right - left, height: bottom - top };
559
+ }
560
+ function visibleBounds(el, memo, initial, throughScrollers = false) {
561
+ let bounds = initial;
562
+ const target = ownVisibilityInfo(el, memo);
563
+ if (target.hidden)
564
+ return null;
565
+ if (null === target.parent || !ownVisibilityInfo(target.parent, memo).hasClipping)
566
+ return bounds;
567
+ let clipFrom;
568
+ for (let current = el; current !== null; current = ownVisibilityInfo(current, memo).parent) {
569
+ const info = ownVisibilityInfo(current, memo);
570
+ if (info.hidden)
571
+ return null;
572
+ if (current === clipFrom)
573
+ clipFrom = undefined;
574
+ const style = info.style;
575
+ if (throughScrollers &&
576
+ current !== el &&
577
+ undefined === clipFrom &&
578
+ (info.scrollX || info.scrollY)) {
579
+ bounds ??= el.getBoundingClientRect();
580
+ if (bounds.width > 0 && bounds.height > 0) {
581
+ bounds = scrolledInto(bounds, clippingBox(current, info), info.scrollX, info.scrollY);
582
+ }
583
+ }
584
+ if (current !== el && undefined === clipFrom && (info.clipX || info.clipY)) {
585
+ bounds ??= el.getBoundingClientRect();
586
+ if (bounds.width > 0 && bounds.height > 0) {
587
+ const clip = clippingBox(current, info);
588
+ const left = info.clipX ? Math.max(bounds.left, clip.left) : bounds.left;
589
+ const right = info.clipX ? Math.min(bounds.right, clip.right) : bounds.right;
590
+ const top = info.clipY ? Math.max(bounds.top, clip.top) : bounds.top;
591
+ const bottom = info.clipY ? Math.min(bounds.bottom, clip.bottom) : bounds.bottom;
592
+ if (right <= left || bottom <= top)
593
+ return null;
594
+ bounds = { left, right, top, bottom, width: right - left, height: bottom - top };
595
+ }
596
+ }
597
+ if (clipFrom === undefined && ('absolute' === style?.position || 'fixed' === style?.position)) {
598
+ clipFrom = positionedContainingBlock(current, 'fixed' === style?.position, memo);
599
+ }
600
+ }
601
+ return bounds;
602
+ }
363
603
  export function isInViewport(el, memo) {
364
604
  if (!isVisible(el, memo))
365
605
  return false;
366
606
  const view = el.ownerDocument.defaultView;
367
607
  if (null === view)
368
608
  return false;
369
- const r = el.getBoundingClientRect();
609
+ const r = visibleBounds(el, visibilityInfoMemo(memo), el.getBoundingClientRect());
610
+ if (null === r || undefined === r)
611
+ return false;
370
612
  if (r.width <= 0 || r.height <= 0)
371
613
  return false;
372
614
  return r.bottom > 0 && r.right > 0 && r.top < view.innerHeight && r.left < view.innerWidth;
@@ -377,8 +619,7 @@ export function isVisible(el, memo) {
377
619
  const cached = memo?.get(el);
378
620
  if (cached !== undefined)
379
621
  return cached;
380
- const parent = parentAcrossShadowBoundary(el);
381
- const result = !selfHidden(el) && (null === parent || isVisible(parent, memo));
622
+ const result = visibleBounds(el, visibilityInfoMemo(memo), undefined, true) !== null;
382
623
  if (memo !== undefined)
383
624
  memo.set(el, result);
384
625
  return result;
@@ -0,0 +1,6 @@
1
+ export declare function coverageMarkers(doc: Document): string[];
2
+ export declare function notApplicableFor(page: {
3
+ buildPlugin: boolean;
4
+ adapters: readonly string[];
5
+ markers: readonly string[];
6
+ }): string[];
@@ -0,0 +1,13 @@
1
+ import { CoverageMarker, SOURCE_MAPPING_CAPABILITY, DATA_RETICLE_SOURCE_ATTR, RETICLE_OVERLAY_SELECTOR, } from '@reticlehq/core';
2
+ import { testIdSelector } from './testid-attr.js';
3
+ const appCarries = (doc, selector) => Array.from(doc.querySelectorAll(selector)).some((el) => null === el.closest(RETICLE_OVERLAY_SELECTOR));
4
+ export function coverageMarkers(doc) {
5
+ return [
6
+ ...(appCarries(doc, `[${DATA_RETICLE_SOURCE_ATTR}]`) ? [CoverageMarker.SOURCE] : []),
7
+ ...(appCarries(doc, testIdSelector()) ? [CoverageMarker.TESTID] : []),
8
+ ];
9
+ }
10
+ export function notApplicableFor(page) {
11
+ const couldStamp = page.buildPlugin || 0 < page.adapters.length || page.markers.includes(CoverageMarker.SOURCE);
12
+ return couldStamp ? [] : [SOURCE_MAPPING_CAPABILITY];
13
+ }
@@ -1,8 +1,10 @@
1
1
  export declare const RETICLE_OVERLAY: string;
2
2
  export declare function setPresenterVisible(visible: boolean): void;
3
+ export declare function setPresenterViewer(harness: boolean): void;
3
4
  export declare function isPresenterVisible(): boolean;
4
5
  export declare function setIgnoreSelectors(selectors: string[]): void;
5
6
  export declare function isReticleOverlay(el: Element): boolean;
7
+ export declare function isUnobserved(el: Element): boolean;
6
8
  export declare function isReticleUi(node: Element | null): boolean;
7
9
  export declare function isIgnored(el: Element): boolean;
8
10
  export declare function appModalOpen(doc: Document): boolean;
@@ -1,10 +1,16 @@
1
- export const RETICLE_OVERLAY = '[data-reticle-overlay],[data-reticle-cursor],[data-reticle-hud],[data-reticle-glow],[data-reticle-mark],[data-reticle-blocker],[data-reticle-tour]';
1
+ import { RETICLE_OVERLAY_SELECTOR } from '@reticlehq/core';
2
+ export const RETICLE_OVERLAY = RETICLE_OVERLAY_SELECTOR;
2
3
  const DEV_OVERLAYS = '[data-agentation],#__next-build-watcher,nextjs-portal,[data-nextjs-dialog],[data-nextjs-toast]';
3
4
  let extraIgnore = '';
4
5
  let presenterVisible = false;
5
6
  export function setPresenterVisible(visible) {
6
7
  presenterVisible = visible;
7
8
  }
9
+ let harnessViewing = false;
10
+ export function setPresenterViewer(harness) {
11
+ harnessViewing = harness;
12
+ }
13
+ const exposed = () => presenterVisible && !harnessViewing;
8
14
  export function isPresenterVisible() {
9
15
  return presenterVisible;
10
16
  }
@@ -14,11 +20,18 @@ export function setIgnoreSelectors(selectors) {
14
20
  export function isReticleOverlay(el) {
15
21
  return el.closest(RETICLE_OVERLAY) !== null;
16
22
  }
23
+ const HUD_DOCK = '[data-reticle-dock]';
24
+ export function isUnobserved(el) {
25
+ if (!isReticleOverlay(el))
26
+ return false;
27
+ return !exposed() || null === el.closest(HUD_DOCK);
28
+ }
17
29
  export function isReticleUi(node) {
18
30
  return node !== null && node.closest(RETICLE_OVERLAY) !== null;
19
31
  }
32
+ const HUD_NARRATION = '[data-reticle-log],[data-reticle-harness-log],[data-reticle-chat-pill-text],[data-reticle-agent-said],[data-reticle-agent-note]';
20
33
  export function isIgnored(el) {
21
- const ours = presenterVisible ? '' : RETICLE_OVERLAY;
34
+ const ours = exposed() ? HUD_NARRATION : RETICLE_OVERLAY;
22
35
  const sel = [ours, DEV_OVERLAYS, extraIgnore].filter((part) => part.length > 0).join(',');
23
36
  return el.closest(sel) !== null;
24
37
  }
@@ -1,5 +1,5 @@
1
1
  import { ElementState, type ElementQuery, type MatchResult, type QueryResult } from '@reticlehq/core';
2
2
  export declare const ATTR_VALUE_MAX = 512;
3
- export declare function matchQuery(query: ElementQuery, state?: ElementState, limit?: number): MatchResult;
3
+ export declare function matchQuery(query: ElementQuery, state?: ElementState, limit?: number, wholePhrase?: boolean): MatchResult;
4
4
  export declare function elementsMatching(query: ElementQuery): HTMLElement[];
5
5
  export declare function runQuery(query: ElementQuery, limit?: number): QueryResult;
package/dist/dom/query.js CHANGED
@@ -24,6 +24,11 @@ function fuzzyVisibleText(actual, expected) {
24
24
  .toLowerCase()
25
25
  .includes(normaliseVisibleText(expected).toLowerCase());
26
26
  }
27
+ function textMatches(actual, query, expected) {
28
+ if (true !== query.matchCase)
29
+ return fuzzyVisibleText(actual, expected);
30
+ return normaliseVisibleText(actual).includes(normaliseVisibleText(expected));
31
+ }
27
32
  function exactVisibleText(actual, expected) {
28
33
  return normaliseVisibleText(actual) === normaliseVisibleText(expected);
29
34
  }
@@ -45,17 +50,29 @@ function directText(el) {
45
50
  .join('');
46
51
  }
47
52
  function visibleTextOf(el, memo) {
48
- if (isNonRendered(el) || isIgnored(el) || !isVisible(el, memo))
53
+ if (isNonRendered(el) || isIgnored(el))
49
54
  return '';
55
+ const visible = isVisible(el, memo);
56
+ const apart = laysOutApart(el);
50
57
  let text = '';
51
58
  for (const node of Array.from(el.childNodes)) {
52
- if (Node.TEXT_NODE === node.nodeType)
59
+ if (Node.TEXT_NODE === node.nodeType && visible)
53
60
  text += node.textContent ?? '';
54
- else if (isElement(node))
55
- text += visibleTextOf(node, memo);
61
+ else if (isElement(node)) {
62
+ const own = visibleTextOf(node, memo);
63
+ text += apart || ownBox(node) ? ` ${own} ` : own;
64
+ }
56
65
  }
57
66
  return text;
58
67
  }
68
+ function laysOutApart(el) {
69
+ const display = el.ownerDocument.defaultView?.getComputedStyle(el).display ?? '';
70
+ return display.includes('flex') || display.includes('grid');
71
+ }
72
+ function ownBox(el) {
73
+ const display = el.ownerDocument.defaultView?.getComputedStyle(el).display ?? 'inline';
74
+ return !display.startsWith('inline') && 'contents' !== display;
75
+ }
59
76
  function semanticNameTarget(el) {
60
77
  if (isInput(el) || isTextArea(el) || isSelect(el))
61
78
  return true;
@@ -126,11 +143,22 @@ function queryByRoleAndName(container, role, name) {
126
143
  return elementsUnder(container).filter((el) => matchesRole(getRole(el), role) &&
127
144
  (name === undefined || exactVisibleText(getAccessibleName(el), name)));
128
145
  }
129
- function queryByText(container, value) {
130
- return elementsUnder(container).filter((el) => fuzzyVisibleText(directText(el), value));
146
+ function queryByText(container, value, query) {
147
+ return elementsUnder(container).filter((el) => textMatches(directText(el), query, value));
131
148
  }
132
149
  function queryByLabel(container, value) {
133
- return elementsUnder(container).filter((el) => semanticNameTarget(el) && fuzzyVisibleText(getAccessibleName(el), value));
150
+ const expected = normaliseVisibleText(value).toLowerCase();
151
+ const exact = [];
152
+ const fuzzy = [];
153
+ for (const el of elementsUnder(container)) {
154
+ if (!semanticNameTarget(el))
155
+ continue;
156
+ const name = normaliseVisibleText(getAccessibleName(el)).toLowerCase();
157
+ if (!name.includes(expected))
158
+ continue;
159
+ (name === expected ? exact : fuzzy).push(el);
160
+ }
161
+ return exact.length > 0 ? exact : fuzzy;
134
162
  }
135
163
  function queryByPlaceholder(container, value) {
136
164
  return elementsUnder(container).filter((el) => {
@@ -155,7 +183,7 @@ function findIn(container, query) {
155
183
  case QueryBy.ROLE:
156
184
  return queryByRoleAndName(container, value, query.name);
157
185
  case QueryBy.TEXT:
158
- return queryByText(container, value);
186
+ return queryByText(container, value, query);
159
187
  case QueryBy.LABEL:
160
188
  return queryByLabel(container, value);
161
189
  case QueryBy.PLACEHOLDER:
@@ -177,7 +205,7 @@ function findIn(container, query) {
177
205
  return queryByRoleAndName(container, query.role, query.name);
178
206
  }
179
207
  if (query.text !== undefined)
180
- return queryByText(container, query.text);
208
+ return queryByText(container, query.text, query);
181
209
  if (query.label !== undefined)
182
210
  return queryByLabel(container, query.label);
183
211
  if (query.placeholder !== undefined)
@@ -229,7 +257,7 @@ function findCandidates(query) {
229
257
  if (query.scope === undefined)
230
258
  return { candidates: [], scopeMissing };
231
259
  if (query.text !== undefined &&
232
- !fuzzyVisibleText(visibleTextOf(container, new Map()), query.text)) {
260
+ !textMatches(visibleTextOf(container, new Map()), query, query.text)) {
233
261
  return { candidates: [], scopeMissing };
234
262
  }
235
263
  return { candidates: isIgnored(container) ? [] : [container], scopeMissing };
@@ -269,10 +297,13 @@ function inState(el, state, memo) {
269
297
  return getStates(el, isVisible(el, memo)).includes(state);
270
298
  }
271
299
  const MAX_DESCRIBED = TRANSPORT_LIMITS.MAX_COLLECTION_ITEMS;
272
- export function matchQuery(query, state, limit = MAX_DESCRIBED) {
300
+ export function matchQuery(query, state, limit = MAX_DESCRIBED, wholePhrase = true) {
273
301
  const found = findCandidates(query);
274
- const elements = found.candidates;
275
302
  const scopeMissing = found.scopeMissing;
303
+ const wanted = wholePhrase && textOnly(query) ? wantedTextOf(query) : undefined;
304
+ const scope = 0 === found.candidates.length ? resolveContainer(query.scope).container : null;
305
+ const owner = null === scope || wanted === undefined ? undefined : splitTextOwner(scope, wanted, query);
306
+ const elements = owner === undefined ? found.candidates : [owner];
276
307
  const visMemo = new Map();
277
308
  const filtered = state === undefined ? elements : elements.filter((el) => inState(el, state, visMemo));
278
309
  const attrs = query.attrs;
@@ -363,14 +394,14 @@ function buildPresentRegions(query) {
363
394
  }
364
395
  return regions;
365
396
  }
366
- function splitTextOwner(container, wanted) {
397
+ function splitTextOwner(container, wanted, query) {
367
398
  let best;
368
399
  let bestDepth = -1;
369
400
  const memo = new Map();
370
401
  for (const el of elementsUnder(container)) {
371
402
  if (isIgnored(el))
372
403
  continue;
373
- if (!fuzzyVisibleText(visibleTextOf(el, memo), wanted))
404
+ if (!textMatches(visibleTextOf(el, memo), query, wanted))
374
405
  continue;
375
406
  let depth = 0;
376
407
  for (let parent = el.parentElement; parent !== null; parent = parent.parentElement)
@@ -382,6 +413,32 @@ function splitTextOwner(container, wanted) {
382
413
  }
383
414
  return best;
384
415
  }
416
+ function caseOnlyMatch(container, wanted) {
417
+ const target = normaliseVisibleText(wanted).toLowerCase();
418
+ if (0 === target.length)
419
+ return undefined;
420
+ const memo = new Map();
421
+ for (const el of elementsUnder(container)) {
422
+ if (isIgnored(el))
423
+ continue;
424
+ const text = normaliseVisibleText(visibleTextOf(el, memo));
425
+ const at = text.toLowerCase().indexOf(target);
426
+ if (-1 !== at)
427
+ return text.slice(at, at + target.length);
428
+ }
429
+ return undefined;
430
+ }
431
+ const TEXT_ONLY_FIELDS = new Set([
432
+ 'text',
433
+ 'matchCase',
434
+ 'scope',
435
+ 'by',
436
+ 'value',
437
+ ]);
438
+ function textOnly(query) {
439
+ return ((query.by === undefined || QueryBy.TEXT === query.by) &&
440
+ Object.keys(query).every((key) => TEXT_ONLY_FIELDS.has(key)));
441
+ }
385
442
  function wantedTextOf(query) {
386
443
  if (query.text !== undefined)
387
444
  return query.text;
@@ -453,9 +510,14 @@ function buildEmptyHint(query) {
453
510
  };
454
511
  const wanted = wantedTextOf(query);
455
512
  if (wanted !== undefined) {
456
- const owner = splitTextOwner(container, wanted);
513
+ const owner = splitTextOwner(container, wanted, query);
457
514
  if (owner !== undefined)
458
515
  hint.splitText = describe(owner);
516
+ else if (true === query.matchCase) {
517
+ const shown = caseOnlyMatch(container, wanted);
518
+ if (shown !== undefined)
519
+ hint.caseMiss = shown;
520
+ }
459
521
  }
460
522
  const near = nameNearMisses(container, query);
461
523
  if (near.length > 0)
@@ -469,7 +531,7 @@ export function elementsMatching(query) {
469
531
  return findCandidates(query).candidates;
470
532
  }
471
533
  export function runQuery(query, limit) {
472
- const result = matchQuery(query, undefined, limit);
534
+ const result = matchQuery(query, undefined, limit, false);
473
535
  const scopeFields = true === result.scopeMissing ? { scopeMissing: true } : {};
474
536
  if (0 === result.elements.length) {
475
537
  return {
@@ -0,0 +1,12 @@
1
+ export declare const MASK = "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022";
2
+ export type RenderResult = {
3
+ ok: true;
4
+ width: number;
5
+ height: number;
6
+ chunks: string[];
7
+ } | {
8
+ ok: false;
9
+ reason: string;
10
+ };
11
+ export declare function cloneForRender(doc: Document): HTMLElement;
12
+ export declare function renderPage(): Promise<RenderResult>;