@reticlehq/browser 3.8.0 → 3.8.1
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/actions/action-report.js +36 -18
- package/dist/actions/actions.js +66 -36
- package/dist/actions/danger-context.js +27 -16
- package/dist/actions/scroll.d.ts +2 -0
- package/dist/actions/scroll.js +10 -2
- package/dist/dom/a11y.d.ts +0 -4
- package/dist/dom/a11y.js +2 -294
- package/dist/dom/dom-ignore.js +23 -10
- package/dist/dom/query-finders.d.ts +6 -0
- package/dist/dom/query-finders.js +216 -0
- package/dist/dom/query.d.ts +0 -1
- package/dist/dom/query.js +108 -244
- package/dist/dom/snapshot.js +2 -1
- package/dist/dom/visibility.d.ts +7 -0
- package/dist/dom/visibility.js +322 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/observers/dom.js +2 -1
- package/dist/observers/net-detail/network-body.js +29 -9
- package/dist/observers/net-detail/network-event-stream.d.ts +5 -0
- package/dist/observers/net-detail/network-event-stream.js +55 -0
- package/dist/observers/net-detail/network-frames.d.ts +2 -0
- package/dist/observers/net-detail/network-frames.js +39 -0
- package/dist/observers/net-detail/network-response-body.d.ts +3 -0
- package/dist/observers/net-detail/network-response-body.js +97 -0
- package/dist/observers/network.js +95 -105
- package/dist/reticle-inject.js +695 -631
- package/package.json +2 -2
package/dist/dom/a11y.js
CHANGED
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
import { readTestId } from './addressing/testid-attr.js';
|
|
2
2
|
import { DEFAULT_TESTID_ATTR, ElementState, REDACTED_VALUE, } from '@reticlehq/core';
|
|
3
|
-
import { isButton,
|
|
3
|
+
import { isButton, isImage, isInput, isMeter, isOutput, isProgress, isSelect, isTextArea, } from './realm.js';
|
|
4
4
|
import { refs } from './addressing/refs.js';
|
|
5
|
-
import { parentAcrossShadowBoundary } from './shadow-registry.js';
|
|
6
|
-
import { clippingBox, overflowAxes, scrolledInto, } from './paint-context.js';
|
|
7
5
|
import { inspectChart } from './chart.js';
|
|
8
6
|
import { isSensitiveKey } from '../security/serialization.js';
|
|
9
7
|
import { formatSource, sourceFromDom } from './addressing/source.js';
|
|
10
|
-
|
|
11
|
-
const HTML_DETAILS_OPEN_ATTRIBUTE = 'open';
|
|
12
|
-
const HTML_SUMMARY_TAG = 'summary';
|
|
8
|
+
import { HTML_DETAILS_OPEN_ATTRIBUTE, HTML_DETAILS_TAG, HTML_SUMMARY_TAG, isVisible, } from './visibility.js';
|
|
13
9
|
const NAME_FROM_CONTENT = new Set([
|
|
14
10
|
'button',
|
|
15
11
|
'link',
|
|
@@ -364,294 +360,6 @@ export function getValue(el) {
|
|
|
364
360
|
const valueNow = el.getAttribute('aria-valuenow');
|
|
365
361
|
return valueNow ?? undefined;
|
|
366
362
|
}
|
|
367
|
-
function hiddenInsideClosedDetails(el) {
|
|
368
|
-
const parent = el.parentElement;
|
|
369
|
-
if (null === parent) {
|
|
370
|
-
return false;
|
|
371
|
-
}
|
|
372
|
-
const details = parent.closest(HTML_DETAILS_TAG);
|
|
373
|
-
if (null === details || details.hasAttribute(HTML_DETAILS_OPEN_ATTRIBUTE)) {
|
|
374
|
-
return false;
|
|
375
|
-
}
|
|
376
|
-
const summary = Array.from(details.children).find((child) => HTML_SUMMARY_TAG === child.localName);
|
|
377
|
-
return summary === undefined || !summary.contains(el);
|
|
378
|
-
}
|
|
379
|
-
function stalledFade(el) {
|
|
380
|
-
if (!el.ownerDocument.hidden || !('getAnimations' in el)) {
|
|
381
|
-
return false;
|
|
382
|
-
}
|
|
383
|
-
return el.getAnimations().some((a) => 'running' === a.playState || a.pending);
|
|
384
|
-
}
|
|
385
|
-
const OVERLAY_ARIA_HIDDEN = 'data-aria-hidden';
|
|
386
|
-
function hiddenByAnOverlay(el) {
|
|
387
|
-
return el.hasAttribute(OVERLAY_ARIA_HIDDEN);
|
|
388
|
-
}
|
|
389
|
-
function selfCssHidden(el, style) {
|
|
390
|
-
if (isHtmlElement(el) && el.hidden) {
|
|
391
|
-
return true;
|
|
392
|
-
}
|
|
393
|
-
if (hiddenInsideClosedDetails(el)) {
|
|
394
|
-
return true;
|
|
395
|
-
}
|
|
396
|
-
if (style !== null) {
|
|
397
|
-
if ('none' === style.display ||
|
|
398
|
-
'hidden' === style.visibility ||
|
|
399
|
-
'collapse' === style.visibility) {
|
|
400
|
-
return true;
|
|
401
|
-
}
|
|
402
|
-
if (0 === Number.parseFloat(style.opacity || '1') && !stalledFade(el)) {
|
|
403
|
-
return true;
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
return false;
|
|
407
|
-
}
|
|
408
|
-
function selfHidden(el, style) {
|
|
409
|
-
if ('true' === el.getAttribute('aria-hidden') && !hiddenByAnOverlay(el)) {
|
|
410
|
-
return true;
|
|
411
|
-
}
|
|
412
|
-
return selfCssHidden(el, style);
|
|
413
|
-
}
|
|
414
|
-
export function isHiddenByAriaOnly(el) {
|
|
415
|
-
let foundAriaHidden = false;
|
|
416
|
-
let current = el;
|
|
417
|
-
while (null !== current) {
|
|
418
|
-
if ('true' === current.getAttribute('aria-hidden')) {
|
|
419
|
-
foundAriaHidden = true;
|
|
420
|
-
}
|
|
421
|
-
const style = current.ownerDocument.defaultView?.getComputedStyle(current) ?? null;
|
|
422
|
-
if (selfCssHidden(current, style)) {
|
|
423
|
-
return false;
|
|
424
|
-
}
|
|
425
|
-
current = parentAcrossShadowBoundary(current);
|
|
426
|
-
}
|
|
427
|
-
return foundAriaHidden;
|
|
428
|
-
}
|
|
429
|
-
export function isRenderedOutsideAriaHidden(el) {
|
|
430
|
-
let current = el;
|
|
431
|
-
while (null !== current) {
|
|
432
|
-
if ('true' === current.getAttribute('aria-hidden')) {
|
|
433
|
-
return false;
|
|
434
|
-
}
|
|
435
|
-
const style = current.ownerDocument.defaultView?.getComputedStyle(current) ?? null;
|
|
436
|
-
if (selfCssHidden(current, style)) {
|
|
437
|
-
return false;
|
|
438
|
-
}
|
|
439
|
-
current = parentAcrossShadowBoundary(current);
|
|
440
|
-
}
|
|
441
|
-
return true;
|
|
442
|
-
}
|
|
443
|
-
const CLIPPING_OVERFLOW = new Set(['hidden', 'clip']);
|
|
444
|
-
const SCROLLING_OVERFLOW = new Set(['auto', 'scroll']);
|
|
445
|
-
const NON_CLIPPING_DISPLAY = new Set([
|
|
446
|
-
'contents',
|
|
447
|
-
'table-row',
|
|
448
|
-
'table-row-group',
|
|
449
|
-
'table-header-group',
|
|
450
|
-
'table-footer-group',
|
|
451
|
-
'table-column',
|
|
452
|
-
'table-column-group',
|
|
453
|
-
]);
|
|
454
|
-
const TRANSFORM_CONTAINING_BLOCK_PROPERTIES = new Set([
|
|
455
|
-
'transform',
|
|
456
|
-
'translate',
|
|
457
|
-
'rotate',
|
|
458
|
-
'scale',
|
|
459
|
-
'perspective',
|
|
460
|
-
]);
|
|
461
|
-
const CONTAINING_BLOCK_PROPERTIES = new Set([
|
|
462
|
-
...TRANSFORM_CONTAINING_BLOCK_PROPERTIES,
|
|
463
|
-
'filter',
|
|
464
|
-
'backdrop-filter',
|
|
465
|
-
]);
|
|
466
|
-
const CONTAINING_BLOCK_CONTAIN = new Set(['layout', 'paint', 'strict', 'content']);
|
|
467
|
-
const REPLACED_INLINE_TAGS = new Set([
|
|
468
|
-
'img',
|
|
469
|
-
'video',
|
|
470
|
-
'audio',
|
|
471
|
-
'canvas',
|
|
472
|
-
'iframe',
|
|
473
|
-
'embed',
|
|
474
|
-
'object',
|
|
475
|
-
'input',
|
|
476
|
-
'textarea',
|
|
477
|
-
'select',
|
|
478
|
-
]);
|
|
479
|
-
const visibilityInfoMemos = new WeakMap();
|
|
480
|
-
function visibilityInfoMemo(memo) {
|
|
481
|
-
if (memo === undefined) {
|
|
482
|
-
return new Map();
|
|
483
|
-
}
|
|
484
|
-
let info = visibilityInfoMemos.get(memo);
|
|
485
|
-
if (info === undefined) {
|
|
486
|
-
info = new Map();
|
|
487
|
-
visibilityInfoMemos.set(memo, info);
|
|
488
|
-
}
|
|
489
|
-
return info;
|
|
490
|
-
}
|
|
491
|
-
function ownVisibilityInfo(el, memo) {
|
|
492
|
-
let info = memo.get(el);
|
|
493
|
-
if (info === undefined) {
|
|
494
|
-
const style = el.ownerDocument.defaultView?.getComputedStyle(el) ?? null;
|
|
495
|
-
if (selfHidden(el, style)) {
|
|
496
|
-
info = {
|
|
497
|
-
hidden: true,
|
|
498
|
-
style,
|
|
499
|
-
parent: null,
|
|
500
|
-
clipX: false,
|
|
501
|
-
clipY: false,
|
|
502
|
-
scrollX: false,
|
|
503
|
-
scrollY: false,
|
|
504
|
-
hasClipping: false,
|
|
505
|
-
};
|
|
506
|
-
memo.set(el, info);
|
|
507
|
-
return info;
|
|
508
|
-
}
|
|
509
|
-
const parent = parentAcrossShadowBoundary(el);
|
|
510
|
-
const inherited = null === parent ? undefined : ownVisibilityInfo(parent, memo);
|
|
511
|
-
const [overflowX, overflowY] = overflowAxes(style);
|
|
512
|
-
const root = el.ownerDocument.documentElement;
|
|
513
|
-
const viewportOverflow = el === root ||
|
|
514
|
-
(el === el.ownerDocument.body &&
|
|
515
|
-
overflowAxes(ownVisibilityInfo(root, memo).style).every((axis) => 'visible' === axis));
|
|
516
|
-
const hasClipBox = !viewportOverflow &&
|
|
517
|
-
!NON_CLIPPING_DISPLAY.has(style?.display ?? '') &&
|
|
518
|
-
!(isHtmlElement(el) && 'inline' === style?.display);
|
|
519
|
-
const clipX = hasClipBox && CLIPPING_OVERFLOW.has(overflowX);
|
|
520
|
-
const clipY = hasClipBox && CLIPPING_OVERFLOW.has(overflowY);
|
|
521
|
-
info = {
|
|
522
|
-
hidden: true === inherited?.hidden,
|
|
523
|
-
style,
|
|
524
|
-
parent,
|
|
525
|
-
clipX,
|
|
526
|
-
clipY,
|
|
527
|
-
scrollX: hasClipBox && SCROLLING_OVERFLOW.has(overflowX),
|
|
528
|
-
scrollY: hasClipBox && SCROLLING_OVERFLOW.has(overflowY),
|
|
529
|
-
hasClipping: clipX || clipY || true === inherited?.hasClipping,
|
|
530
|
-
};
|
|
531
|
-
memo.set(el, info);
|
|
532
|
-
}
|
|
533
|
-
return info;
|
|
534
|
-
}
|
|
535
|
-
function positionedContainingBlock(el, fixed, memo) {
|
|
536
|
-
for (let parent = ownVisibilityInfo(el, memo).parent; parent !== null; parent = ownVisibilityInfo(parent, memo).parent) {
|
|
537
|
-
const style = ownVisibilityInfo(parent, memo).style;
|
|
538
|
-
if (null === style || 'contents' === style.display) {
|
|
539
|
-
continue;
|
|
540
|
-
}
|
|
541
|
-
if (!fixed && '' !== style.position && 'static' !== style.position) {
|
|
542
|
-
return parent;
|
|
543
|
-
}
|
|
544
|
-
const transformable = !('inline' === style.display &&
|
|
545
|
-
isHtmlElement(parent) &&
|
|
546
|
-
!REPLACED_INLINE_TAGS.has(parent.localName)) &&
|
|
547
|
-
'table-column' !== style.display &&
|
|
548
|
-
'table-column-group' !== style.display;
|
|
549
|
-
const applies = (property) => transformable || !TRANSFORM_CONTAINING_BLOCK_PROPERTIES.has(property);
|
|
550
|
-
if ([...CONTAINING_BLOCK_PROPERTIES].some((property) => {
|
|
551
|
-
const value = style.getPropertyValue(property);
|
|
552
|
-
return applies(property) && '' !== value && 'none' !== value;
|
|
553
|
-
}) ||
|
|
554
|
-
style.willChange
|
|
555
|
-
.split(',')
|
|
556
|
-
.some((property) => CONTAINING_BLOCK_PROPERTIES.has(property.trim()) && applies(property.trim())) ||
|
|
557
|
-
(transformable &&
|
|
558
|
-
!NON_CLIPPING_DISPLAY.has(style.display) &&
|
|
559
|
-
(style.contain.split(/\s+/).some((value) => CONTAINING_BLOCK_CONTAIN.has(value)) ||
|
|
560
|
-
'auto' === style.contentVisibility))) {
|
|
561
|
-
return parent;
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
return null;
|
|
565
|
-
}
|
|
566
|
-
function visibleBounds(el, memo, initial, throughScrollers = false) {
|
|
567
|
-
let bounds = initial;
|
|
568
|
-
const target = ownVisibilityInfo(el, memo);
|
|
569
|
-
if (target.hidden) {
|
|
570
|
-
return null;
|
|
571
|
-
}
|
|
572
|
-
if (null === target.parent || !ownVisibilityInfo(target.parent, memo).hasClipping) {
|
|
573
|
-
return bounds;
|
|
574
|
-
}
|
|
575
|
-
let clipFrom;
|
|
576
|
-
for (let current = el; current !== null; current = ownVisibilityInfo(current, memo).parent) {
|
|
577
|
-
const info = ownVisibilityInfo(current, memo);
|
|
578
|
-
if (info.hidden) {
|
|
579
|
-
return null;
|
|
580
|
-
}
|
|
581
|
-
if (current === clipFrom) {
|
|
582
|
-
clipFrom = undefined;
|
|
583
|
-
}
|
|
584
|
-
const style = info.style;
|
|
585
|
-
if (throughScrollers &&
|
|
586
|
-
current !== el &&
|
|
587
|
-
undefined === clipFrom &&
|
|
588
|
-
(info.scrollX || info.scrollY)) {
|
|
589
|
-
bounds ??= el.getBoundingClientRect();
|
|
590
|
-
if (bounds.width > 0 && bounds.height > 0) {
|
|
591
|
-
bounds = scrolledInto(bounds, clippingBox(current, info), info.scrollX, info.scrollY);
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
if (current !== el && undefined === clipFrom && (info.clipX || info.clipY)) {
|
|
595
|
-
bounds ??= el.getBoundingClientRect();
|
|
596
|
-
if (bounds.width > 0 && bounds.height > 0) {
|
|
597
|
-
const clip = clippingBox(current, info);
|
|
598
|
-
const left = info.clipX ? Math.max(bounds.left, clip.left) : bounds.left;
|
|
599
|
-
const right = info.clipX ? Math.min(bounds.right, clip.right) : bounds.right;
|
|
600
|
-
const top = info.clipY ? Math.max(bounds.top, clip.top) : bounds.top;
|
|
601
|
-
const bottom = info.clipY ? Math.min(bounds.bottom, clip.bottom) : bounds.bottom;
|
|
602
|
-
if (right <= left || bottom <= top) {
|
|
603
|
-
return null;
|
|
604
|
-
}
|
|
605
|
-
bounds = { left, right, top, bottom, width: right - left, height: bottom - top };
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
if (clipFrom === undefined && ('absolute' === style?.position || 'fixed' === style?.position)) {
|
|
609
|
-
clipFrom = positionedContainingBlock(current, 'fixed' === style?.position, memo);
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
return bounds;
|
|
613
|
-
}
|
|
614
|
-
export function isInViewport(el, memo) {
|
|
615
|
-
if (!isVisible(el, memo)) {
|
|
616
|
-
return false;
|
|
617
|
-
}
|
|
618
|
-
const view = el.ownerDocument.defaultView;
|
|
619
|
-
if (null === view) {
|
|
620
|
-
return false;
|
|
621
|
-
}
|
|
622
|
-
const r = visibleBounds(el, visibilityInfoMemo(memo), el.getBoundingClientRect());
|
|
623
|
-
if (null === r || undefined === r) {
|
|
624
|
-
return false;
|
|
625
|
-
}
|
|
626
|
-
if (r.width <= 0 || r.height <= 0) {
|
|
627
|
-
return false;
|
|
628
|
-
}
|
|
629
|
-
return r.bottom > 0 && r.right > 0 && r.top < view.innerHeight && r.left < view.innerWidth;
|
|
630
|
-
}
|
|
631
|
-
function collapsed(el, memo) {
|
|
632
|
-
const own = ownVisibilityInfo(el, memo);
|
|
633
|
-
if (!own.clipX && !own.clipY) {
|
|
634
|
-
return false;
|
|
635
|
-
}
|
|
636
|
-
const rects = Array.from(el.getClientRects());
|
|
637
|
-
return (rects.length > 0 &&
|
|
638
|
-
rects.every((r) => (own.clipX && 0 === r.width) || (own.clipY && 0 === r.height)));
|
|
639
|
-
}
|
|
640
|
-
export function isVisible(el, memo) {
|
|
641
|
-
if (!el.isConnected) {
|
|
642
|
-
return false;
|
|
643
|
-
}
|
|
644
|
-
const cached = memo?.get(el);
|
|
645
|
-
if (cached !== undefined) {
|
|
646
|
-
return cached;
|
|
647
|
-
}
|
|
648
|
-
const info = visibilityInfoMemo(memo);
|
|
649
|
-
const result = visibleBounds(el, info, undefined, true) !== null && !collapsed(el, info);
|
|
650
|
-
if (memo !== undefined) {
|
|
651
|
-
memo.set(el, result);
|
|
652
|
-
}
|
|
653
|
-
return result;
|
|
654
|
-
}
|
|
655
363
|
const MAX_TEXT = 80;
|
|
656
364
|
function getVisibleText(el) {
|
|
657
365
|
const text = collapse(el.textContent ?? '');
|
package/dist/dom/dom-ignore.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { RETICLE_OVERLAY_SELECTOR } from '@reticlehq/core';
|
|
2
2
|
import { capturedRootOf } from './shadow-registry.js';
|
|
3
|
+
import { isVisible } from './visibility.js';
|
|
3
4
|
export const RETICLE_OVERLAY = RETICLE_OVERLAY_SELECTOR;
|
|
4
5
|
const DEV_OVERLAYS = '[data-agentation],#__next-build-watcher,nextjs-portal,[data-nextjs-dialog],[data-nextjs-toast]';
|
|
5
6
|
let extraIgnore = '';
|
|
@@ -23,8 +24,9 @@ export function isReticleOverlay(el) {
|
|
|
23
24
|
}
|
|
24
25
|
const HUD_DOCK = '[data-reticle-dock]';
|
|
25
26
|
export function isUnobserved(el) {
|
|
26
|
-
if (!isReticleOverlay(el))
|
|
27
|
+
if (!isReticleOverlay(el)) {
|
|
27
28
|
return false;
|
|
29
|
+
}
|
|
28
30
|
return !exposed() || null === el.closest(HUD_DOCK);
|
|
29
31
|
}
|
|
30
32
|
export function isReticleUi(node) {
|
|
@@ -48,30 +50,38 @@ const NEXT_DIALOG = '[data-nextjs-dialog]';
|
|
|
48
50
|
const VITE_OVERLAY = 'vite-error-overlay';
|
|
49
51
|
function openFrameworkErrorDialog(doc) {
|
|
50
52
|
const vite = doc.querySelector(VITE_OVERLAY);
|
|
51
|
-
if (vite !== null)
|
|
53
|
+
if (vite !== null) {
|
|
52
54
|
return { name: FrameworkErrorDialog.VITE, host: vite };
|
|
55
|
+
}
|
|
53
56
|
for (const portal of Array.from(doc.querySelectorAll(NEXT_PORTAL))) {
|
|
54
57
|
const root = portal.shadowRoot ?? capturedRootOf(portal);
|
|
55
|
-
|
|
58
|
+
const dialog = (root ?? portal).querySelector(NEXT_DIALOG);
|
|
59
|
+
if (null !== dialog && isVisible(dialog)) {
|
|
56
60
|
return { name: FrameworkErrorDialog.NEXT, host: portal };
|
|
57
61
|
}
|
|
58
62
|
}
|
|
59
63
|
const light = doc.querySelector(NEXT_DIALOG);
|
|
60
|
-
|
|
64
|
+
if (null === light || !isVisible(light)) {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
return { name: FrameworkErrorDialog.NEXT, host: light };
|
|
61
68
|
}
|
|
62
69
|
export function dialogCovering(matches) {
|
|
63
70
|
const first = matches[0];
|
|
64
|
-
if (first === undefined)
|
|
71
|
+
if (first === undefined) {
|
|
65
72
|
return undefined;
|
|
73
|
+
}
|
|
66
74
|
const dialog = openFrameworkErrorDialog(first.ownerDocument);
|
|
67
|
-
if (dialog === undefined)
|
|
75
|
+
if (dialog === undefined) {
|
|
68
76
|
return undefined;
|
|
77
|
+
}
|
|
69
78
|
return matches.some((el) => !isInsideHost(el, dialog.host)) ? dialog.name : undefined;
|
|
70
79
|
}
|
|
71
80
|
function isInsideHost(el, host) {
|
|
72
81
|
for (let node = el; node !== null;) {
|
|
73
|
-
if (node === host)
|
|
82
|
+
if (node === host) {
|
|
74
83
|
return true;
|
|
84
|
+
}
|
|
75
85
|
const root = node.getRootNode();
|
|
76
86
|
node = node.parentNode ?? (root instanceof ShadowRoot ? root.host : null);
|
|
77
87
|
}
|
|
@@ -80,12 +90,15 @@ function isInsideHost(el, host) {
|
|
|
80
90
|
const APP_MODAL = 'dialog[open], [aria-modal="true"]';
|
|
81
91
|
export function appModalOpen(doc) {
|
|
82
92
|
for (const el of Array.from(doc.querySelectorAll(APP_MODAL))) {
|
|
83
|
-
if (el.closest(RETICLE_OVERLAY) !== null)
|
|
93
|
+
if (el.closest(RETICLE_OVERLAY) !== null) {
|
|
84
94
|
continue;
|
|
85
|
-
|
|
95
|
+
}
|
|
96
|
+
if (el instanceof HTMLElement && (el.hidden || null !== el.closest('[hidden]'))) {
|
|
86
97
|
continue;
|
|
87
|
-
|
|
98
|
+
}
|
|
99
|
+
if (el instanceof HTMLElement && 'none' === getComputedStyle(el).display) {
|
|
88
100
|
continue;
|
|
101
|
+
}
|
|
89
102
|
return true;
|
|
90
103
|
}
|
|
91
104
|
return false;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type ElementQuery } from '@reticlehq/core';
|
|
2
|
+
export declare function elementsUnder(container: HTMLElement): HTMLElement[];
|
|
3
|
+
export declare function isNonRendered(el: Element): boolean;
|
|
4
|
+
export declare function isHiddenNativeOption(el: HTMLElement): boolean;
|
|
5
|
+
export declare function queryByRoleAndName(container: HTMLElement, role: string, name: string | undefined): HTMLElement[];
|
|
6
|
+
export declare function findIn(container: HTMLElement, query: ElementQuery): HTMLElement[];
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { DATA_RETICLE_SOURCE_ATTR, QueryBy } from '@reticlehq/core';
|
|
2
|
+
import { isInput, isSelect, isTextArea } from './realm.js';
|
|
3
|
+
import { getAccessibleName, getRole } from './a11y.js';
|
|
4
|
+
import { isVisible } from './visibility.js';
|
|
5
|
+
import { identifyComponent } from '../registry/stores/adapters.js';
|
|
6
|
+
import { readTestId, testIdSelector } from './addressing/testid-attr.js';
|
|
7
|
+
import { exactVisibleText, fuzzyVisibleText, comparableText, paintedCase, textMatches, } from './text-match.js';
|
|
8
|
+
const SOURCE_ATTR = DATA_RETICLE_SOURCE_ATTR;
|
|
9
|
+
const MAX_COMPONENT_CANDIDATES = 2000;
|
|
10
|
+
const componentCandidateSelector = () => `[${SOURCE_ATTR}], ${testIdSelector()}, button, a, input, select, textarea, [role]`;
|
|
11
|
+
export function elementsUnder(container) {
|
|
12
|
+
const self = Node.ELEMENT_NODE === container.nodeType ? [container] : [];
|
|
13
|
+
return [...self, ...Array.from(container.querySelectorAll('*'))];
|
|
14
|
+
}
|
|
15
|
+
const NON_RENDERED_TAGS = ['script', 'style', 'template', 'noscript'];
|
|
16
|
+
export function isNonRendered(el) {
|
|
17
|
+
return NON_RENDERED_TAGS.includes(el.tagName.toLowerCase());
|
|
18
|
+
}
|
|
19
|
+
function directText(el) {
|
|
20
|
+
if (isInput(el)) {
|
|
21
|
+
const type = el.type.toLowerCase();
|
|
22
|
+
if ('submit' === type || 'button' === type || 'reset' === type) {
|
|
23
|
+
return el.value;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (isNonRendered(el)) {
|
|
27
|
+
return '';
|
|
28
|
+
}
|
|
29
|
+
return Array.from(el.childNodes)
|
|
30
|
+
.filter((node) => Node.TEXT_NODE === node.nodeType)
|
|
31
|
+
.map((node) => node.textContent ?? '')
|
|
32
|
+
.join('');
|
|
33
|
+
}
|
|
34
|
+
function semanticNameTarget(el) {
|
|
35
|
+
if (isInput(el) || isTextArea(el) || isSelect(el)) {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
const tag = el.tagName.toLowerCase();
|
|
39
|
+
return ('button' === tag ||
|
|
40
|
+
'meter' === tag ||
|
|
41
|
+
'output' === tag ||
|
|
42
|
+
'progress' === tag ||
|
|
43
|
+
el.hasAttribute('aria-label') ||
|
|
44
|
+
el.hasAttribute('aria-labelledby'));
|
|
45
|
+
}
|
|
46
|
+
function findBySource(container, source) {
|
|
47
|
+
const prefix = `${source.file}:${source.line}:`.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
48
|
+
try {
|
|
49
|
+
return Array.from(container.querySelectorAll(`[${SOURCE_ATTR}^="${prefix}"]`));
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function findByComponentName(container, component) {
|
|
56
|
+
const out = [];
|
|
57
|
+
let scanned = 0;
|
|
58
|
+
for (const el of Array.from(container.querySelectorAll(componentCandidateSelector()))) {
|
|
59
|
+
if (scanned >= MAX_COMPONENT_CANDIDATES) {
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
scanned += 1;
|
|
63
|
+
const info = identifyComponent(el);
|
|
64
|
+
if (info !== null && info.componentStack[0] === component) {
|
|
65
|
+
out.push(el);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
function findByComponent(container, query) {
|
|
71
|
+
if (query.source !== undefined) {
|
|
72
|
+
const bySource = findBySource(container, query.source);
|
|
73
|
+
if (bySource.length > 0) {
|
|
74
|
+
return bySource;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (query.component !== undefined && query.component.length > 0) {
|
|
78
|
+
return findByComponentName(container, query.component);
|
|
79
|
+
}
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
function matchesRole(actual, queried) {
|
|
83
|
+
if (actual === queried) {
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
if ('textbox' === queried && 'searchbox' === actual) {
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
const OPTION_ROLE = 'option';
|
|
92
|
+
const HTML_OPTION_TAG = 'option';
|
|
93
|
+
const HTML_SELECT_TAG = 'select';
|
|
94
|
+
const VISUALLY_HIDDEN_MAX_PX = 1;
|
|
95
|
+
export function isHiddenNativeOption(el) {
|
|
96
|
+
if (HTML_OPTION_TAG !== el.localName) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
const select = el.closest(HTML_SELECT_TAG);
|
|
100
|
+
if (!isSelect(select)) {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
if (!isVisible(select)) {
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
return isVisuallyHiddenBox(select);
|
|
107
|
+
}
|
|
108
|
+
function isVisuallyHiddenBox(select) {
|
|
109
|
+
const box = select.getBoundingClientRect();
|
|
110
|
+
return (box.width > 0 &&
|
|
111
|
+
box.height > 0 &&
|
|
112
|
+
box.width <= VISUALLY_HIDDEN_MAX_PX &&
|
|
113
|
+
box.height <= VISUALLY_HIDDEN_MAX_PX);
|
|
114
|
+
}
|
|
115
|
+
function preferVisibleActionableOption(role, matches) {
|
|
116
|
+
if (OPTION_ROLE !== role) {
|
|
117
|
+
return matches;
|
|
118
|
+
}
|
|
119
|
+
const kept = matches.filter((el) => !isHiddenNativeOption(el));
|
|
120
|
+
return kept.length === matches.length ? matches : kept;
|
|
121
|
+
}
|
|
122
|
+
export function queryByRoleAndName(container, role, name) {
|
|
123
|
+
const matched = elementsUnder(container).filter((el) => matchesRole(getRole(el), role) &&
|
|
124
|
+
(name === undefined || exactVisibleText(getAccessibleName(el), name)));
|
|
125
|
+
return preferVisibleActionableOption(role, matched);
|
|
126
|
+
}
|
|
127
|
+
function queryByText(container, value, query) {
|
|
128
|
+
return elementsUnder(container).filter((el) => {
|
|
129
|
+
const own = directText(el);
|
|
130
|
+
if (textMatches(own, query, value)) {
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
if (true !== query.matchCase || !fuzzyVisibleText(own, value)) {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
const painted = paintedCase(el, own);
|
|
137
|
+
return painted !== undefined && textMatches(painted, query, value);
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
function queryByLabel(container, value) {
|
|
141
|
+
const expected = comparableText(value).toLowerCase();
|
|
142
|
+
const exact = [];
|
|
143
|
+
const fuzzy = [];
|
|
144
|
+
for (const el of elementsUnder(container)) {
|
|
145
|
+
if (!semanticNameTarget(el)) {
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const name = comparableText(getAccessibleName(el)).toLowerCase();
|
|
149
|
+
if (!name.includes(expected)) {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
(name === expected ? exact : fuzzy).push(el);
|
|
153
|
+
}
|
|
154
|
+
return exact.length > 0 ? exact : fuzzy;
|
|
155
|
+
}
|
|
156
|
+
function queryByPlaceholder(container, value) {
|
|
157
|
+
return elementsUnder(container).filter((el) => {
|
|
158
|
+
const placeholder = el.getAttribute('placeholder');
|
|
159
|
+
return placeholder !== null && fuzzyVisibleText(placeholder, value);
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
function queryByTestId(container, value) {
|
|
163
|
+
return elementsUnder(container).filter((el) => readTestId(el) === value);
|
|
164
|
+
}
|
|
165
|
+
function queryByAlt(container, value) {
|
|
166
|
+
return elementsUnder(container).filter((el) => {
|
|
167
|
+
const alt = el.getAttribute('alt');
|
|
168
|
+
return alt !== null && fuzzyVisibleText(alt, value);
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
export function findIn(container, query) {
|
|
172
|
+
const by = query.by;
|
|
173
|
+
const value = query.value;
|
|
174
|
+
if (by !== undefined && value !== undefined) {
|
|
175
|
+
switch (by) {
|
|
176
|
+
case QueryBy.ROLE:
|
|
177
|
+
return queryByRoleAndName(container, value, query.name);
|
|
178
|
+
case QueryBy.TEXT:
|
|
179
|
+
return queryByText(container, value, query);
|
|
180
|
+
case QueryBy.LABEL:
|
|
181
|
+
return queryByLabel(container, value);
|
|
182
|
+
case QueryBy.PLACEHOLDER:
|
|
183
|
+
return queryByPlaceholder(container, value);
|
|
184
|
+
case QueryBy.TESTID:
|
|
185
|
+
return queryByTestId(container, value);
|
|
186
|
+
case QueryBy.ALT:
|
|
187
|
+
return queryByAlt(container, value);
|
|
188
|
+
case QueryBy.COMPONENT:
|
|
189
|
+
return findByComponent(container, { ...query, component: query.component ?? value });
|
|
190
|
+
default:
|
|
191
|
+
throw new Error(`unsupported query strategy '${String(by)}' - use one of: ${Object.values(QueryBy).join(', ')}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (query.component !== undefined || query.source !== undefined) {
|
|
195
|
+
return findByComponent(container, query);
|
|
196
|
+
}
|
|
197
|
+
if (query.role !== undefined) {
|
|
198
|
+
return queryByRoleAndName(container, query.role, query.name);
|
|
199
|
+
}
|
|
200
|
+
if (query.text !== undefined) {
|
|
201
|
+
return queryByText(container, query.text, query);
|
|
202
|
+
}
|
|
203
|
+
if (query.label !== undefined) {
|
|
204
|
+
return queryByLabel(container, query.label);
|
|
205
|
+
}
|
|
206
|
+
if (query.placeholder !== undefined) {
|
|
207
|
+
return queryByPlaceholder(container, query.placeholder);
|
|
208
|
+
}
|
|
209
|
+
if (query.testid !== undefined) {
|
|
210
|
+
return queryByTestId(container, query.testid);
|
|
211
|
+
}
|
|
212
|
+
if (query.alt !== undefined) {
|
|
213
|
+
return queryByAlt(container, query.alt);
|
|
214
|
+
}
|
|
215
|
+
return [];
|
|
216
|
+
}
|
package/dist/dom/query.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { ElementState, type ElementQuery, type MatchResult, type QueryResult } from '@reticlehq/core';
|
|
2
|
-
export declare function isHiddenNativeOption(el: HTMLElement): boolean;
|
|
3
2
|
export declare const ATTR_VALUE_MAX = 512;
|
|
4
3
|
export declare function matchQuery(query: ElementQuery, state?: ElementState, limit?: number, wholePhrase?: boolean, diagnose?: boolean): MatchResult;
|
|
5
4
|
export declare function elementsMatching(query: ElementQuery): HTMLElement[];
|