@geometra/proxy 1.19.20 → 1.19.23
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/a11y-enrich.d.ts +9 -2
- package/dist/a11y-enrich.d.ts.map +1 -1
- package/dist/a11y-enrich.js +204 -85
- package/dist/a11y-enrich.js.map +1 -1
- package/dist/dom-actions.d.ts.map +1 -1
- package/dist/dom-actions.js +654 -30
- package/dist/dom-actions.js.map +1 -1
- package/dist/extractor.d.ts +16 -1
- package/dist/extractor.d.ts.map +1 -1
- package/dist/extractor.js +108 -23
- package/dist/extractor.js.map +1 -1
- package/dist/geometry-ws.d.ts +22 -1
- package/dist/geometry-ws.d.ts.map +1 -1
- package/dist/geometry-ws.js +111 -22
- package/dist/geometry-ws.js.map +1 -1
- package/dist/index.js +12 -3
- package/dist/index.js.map +1 -1
- package/dist/runtime.d.ts +15 -3
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +69 -35
- package/dist/runtime.js.map +1 -1
- package/package.json +1 -1
package/dist/dom-actions.js
CHANGED
|
@@ -4,7 +4,26 @@ function delay(ms) {
|
|
|
4
4
|
return new Promise(r => setTimeout(r, ms));
|
|
5
5
|
}
|
|
6
6
|
const LABELED_CONTROL_SELECTOR = 'input, select, textarea, button, [role="combobox"], [role="textbox"], [aria-haspopup="listbox"], [contenteditable="true"]';
|
|
7
|
-
const
|
|
7
|
+
const POPUP_CONTAINER_SELECTOR = '[role="listbox"], [role="menu"], [role="dialog"], [aria-modal="true"], [data-radix-popper-content-wrapper], [class*="menu"], [class*="option"], [class*="select"], [class*="dropdown"]';
|
|
8
|
+
const POPUP_ROOT_SELECTOR = '[role="listbox"], [role="menu"], [role="dialog"], [aria-modal="true"], [data-radix-popper-content-wrapper], [class*="menu"], [class*="dropdown"], [class*="popover"], [class*="listbox"], [class*="options"]';
|
|
9
|
+
const OPTION_PICKER_SELECTOR = [
|
|
10
|
+
'[role="option"]',
|
|
11
|
+
'[role="menuitem"]',
|
|
12
|
+
'[role="treeitem"]',
|
|
13
|
+
'button',
|
|
14
|
+
'li',
|
|
15
|
+
'[data-value]',
|
|
16
|
+
'[aria-selected]',
|
|
17
|
+
'[aria-checked]',
|
|
18
|
+
'[role="listbox"] > *',
|
|
19
|
+
'[role="menu"] > *',
|
|
20
|
+
'[class*="option"]',
|
|
21
|
+
'[class*="menu-item"]',
|
|
22
|
+
'[class*="dropdown-item"]',
|
|
23
|
+
'[class*="listbox-option"]',
|
|
24
|
+
].join(', ');
|
|
25
|
+
const MAX_VISIBLE_OPTION_HINTS = 12;
|
|
26
|
+
const LISTBOX_KEYBOARD_FALLBACK_STEPS = 40;
|
|
8
27
|
export function createFillLookupCache() {
|
|
9
28
|
return {
|
|
10
29
|
control: new Map(),
|
|
@@ -52,7 +71,18 @@ function writeCachedLocator(cache, kind, label, exact, fieldId, locator) {
|
|
|
52
71
|
}
|
|
53
72
|
}
|
|
54
73
|
function normalizedOptionLabel(value) {
|
|
55
|
-
return value
|
|
74
|
+
return value
|
|
75
|
+
.normalize('NFKD')
|
|
76
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
77
|
+
.replace(/[+﹢∔]/g, '+')
|
|
78
|
+
.replace(/[‐‑‒–—―]/g, '-')
|
|
79
|
+
.replace(/&/g, ' and ')
|
|
80
|
+
.replace(/\bplus\b/g, '+')
|
|
81
|
+
.replace(/[,/()]+/g, ' ')
|
|
82
|
+
.replace(/\s*\+\s*/g, '+')
|
|
83
|
+
.replace(/\s+/g, ' ')
|
|
84
|
+
.trim()
|
|
85
|
+
.toLowerCase();
|
|
56
86
|
}
|
|
57
87
|
function prefersGroupedChoiceValue(value) {
|
|
58
88
|
const normalized = normalizedOptionLabel(value);
|
|
@@ -82,6 +112,30 @@ function semanticSelectionAliases(value) {
|
|
|
82
112
|
aliases.add(alias);
|
|
83
113
|
}
|
|
84
114
|
}
|
|
115
|
+
if (normalized === 'atx' || normalized.includes('austin')) {
|
|
116
|
+
for (const alias of ['atx', 'austin', 'austin tx', 'austin texas'])
|
|
117
|
+
aliases.add(alias);
|
|
118
|
+
}
|
|
119
|
+
if (normalized === 'nyc' || normalized.includes('new york')) {
|
|
120
|
+
for (const alias of ['nyc', 'new york', 'new york ny'])
|
|
121
|
+
aliases.add(alias);
|
|
122
|
+
}
|
|
123
|
+
if (normalized === 'sf' || normalized.includes('san francisco')) {
|
|
124
|
+
for (const alias of ['sf', 'san francisco', 'san francisco ca'])
|
|
125
|
+
aliases.add(alias);
|
|
126
|
+
}
|
|
127
|
+
if (normalized === 'la' || normalized.includes('los angeles')) {
|
|
128
|
+
for (const alias of ['la', 'los angeles', 'los angeles ca'])
|
|
129
|
+
aliases.add(alias);
|
|
130
|
+
}
|
|
131
|
+
if (normalized === 'dc' || normalized.includes('washington dc')) {
|
|
132
|
+
for (const alias of ['dc', 'washington dc', 'washington d c'])
|
|
133
|
+
aliases.add(alias);
|
|
134
|
+
}
|
|
135
|
+
if (normalized === 'us' || normalized === 'usa' || normalized.includes('united states')) {
|
|
136
|
+
for (const alias of ['us', 'usa', 'united states'])
|
|
137
|
+
aliases.add(alias);
|
|
138
|
+
}
|
|
85
139
|
return [...aliases];
|
|
86
140
|
}
|
|
87
141
|
function hasNegativeSelectionCue(value) {
|
|
@@ -290,6 +344,7 @@ async function resolveMeaningfulClickTarget(locator) {
|
|
|
290
344
|
async function findLabeledControl(frame, fieldLabel, exact, opts) {
|
|
291
345
|
const directCandidates = [
|
|
292
346
|
frame.getByLabel(fieldLabel, { exact }),
|
|
347
|
+
frame.getByPlaceholder(fieldLabel, { exact }),
|
|
293
348
|
frame.getByRole('combobox', { name: fieldLabel, exact }),
|
|
294
349
|
frame.getByRole('textbox', { name: fieldLabel, exact }),
|
|
295
350
|
frame.getByRole('button', { name: fieldLabel, exact }),
|
|
@@ -354,6 +409,25 @@ async function findLabeledControl(frame, fieldLabel, exact, opts) {
|
|
|
354
409
|
if (text)
|
|
355
410
|
return text;
|
|
356
411
|
}
|
|
412
|
+
if (el.parentElement?.tagName.toLowerCase() === 'label') {
|
|
413
|
+
const text = el.parentElement.textContent?.trim();
|
|
414
|
+
if (text)
|
|
415
|
+
return text;
|
|
416
|
+
}
|
|
417
|
+
if ((el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) &&
|
|
418
|
+
!['checkbox', 'radio', 'file', 'button', 'submit', 'reset', 'hidden'].includes(el instanceof HTMLInputElement ? el.type : '')) {
|
|
419
|
+
const placeholder = el.getAttribute('aria-placeholder')?.trim() || el.getAttribute('placeholder')?.trim();
|
|
420
|
+
if (placeholder)
|
|
421
|
+
return placeholder;
|
|
422
|
+
}
|
|
423
|
+
if (el instanceof HTMLInputElement && ['button', 'submit', 'reset'].includes(el.type)) {
|
|
424
|
+
const value = el.value?.trim();
|
|
425
|
+
if (value)
|
|
426
|
+
return value;
|
|
427
|
+
}
|
|
428
|
+
const title = el.getAttribute('title')?.trim();
|
|
429
|
+
if (title)
|
|
430
|
+
return title;
|
|
357
431
|
return undefined;
|
|
358
432
|
}
|
|
359
433
|
function controlPriority(el) {
|
|
@@ -512,6 +586,362 @@ async function typeIntoActiveEditableElement(page, text) {
|
|
|
512
586
|
}
|
|
513
587
|
return false;
|
|
514
588
|
}
|
|
589
|
+
async function clearEditableLocator(locator) {
|
|
590
|
+
try {
|
|
591
|
+
await locator.fill('');
|
|
592
|
+
return true;
|
|
593
|
+
}
|
|
594
|
+
catch {
|
|
595
|
+
/* fall through */
|
|
596
|
+
}
|
|
597
|
+
try {
|
|
598
|
+
await locator.click();
|
|
599
|
+
await locator.press('ControlOrMeta+A');
|
|
600
|
+
await locator.press('Backspace');
|
|
601
|
+
return true;
|
|
602
|
+
}
|
|
603
|
+
catch {
|
|
604
|
+
return false;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
async function clearActiveEditableElement(page) {
|
|
608
|
+
for (const frame of page.frames()) {
|
|
609
|
+
const cleared = await frame.evaluate(() => {
|
|
610
|
+
const active = document.activeElement;
|
|
611
|
+
if (active instanceof HTMLInputElement || active instanceof HTMLTextAreaElement) {
|
|
612
|
+
active.value = '';
|
|
613
|
+
active.dispatchEvent(new Event('input', { bubbles: true }));
|
|
614
|
+
return true;
|
|
615
|
+
}
|
|
616
|
+
if (active instanceof HTMLElement && active.isContentEditable) {
|
|
617
|
+
active.textContent = '';
|
|
618
|
+
active.dispatchEvent(new Event('input', { bubbles: true }));
|
|
619
|
+
return true;
|
|
620
|
+
}
|
|
621
|
+
return false;
|
|
622
|
+
});
|
|
623
|
+
if (cleared)
|
|
624
|
+
return true;
|
|
625
|
+
}
|
|
626
|
+
return false;
|
|
627
|
+
}
|
|
628
|
+
async function resetTypedListboxQuery(page, locator) {
|
|
629
|
+
if (locator && await clearEditableLocator(locator))
|
|
630
|
+
return true;
|
|
631
|
+
return clearActiveEditableElement(page);
|
|
632
|
+
}
|
|
633
|
+
async function resolveMeaningfulOptionClickTarget(locator) {
|
|
634
|
+
const baseHandle = await locator.elementHandle();
|
|
635
|
+
if (!baseHandle)
|
|
636
|
+
return null;
|
|
637
|
+
const targetHandle = (await baseHandle.evaluateHandle((el, payload) => {
|
|
638
|
+
function visible(node) {
|
|
639
|
+
if (!(node instanceof HTMLElement))
|
|
640
|
+
return false;
|
|
641
|
+
const rect = node.getBoundingClientRect();
|
|
642
|
+
if (rect.width <= 0 || rect.height <= 0)
|
|
643
|
+
return false;
|
|
644
|
+
const style = getComputedStyle(node);
|
|
645
|
+
return style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0';
|
|
646
|
+
}
|
|
647
|
+
function textFor(node) {
|
|
648
|
+
return node.getAttribute('aria-label')?.trim() || node.textContent?.trim() || '';
|
|
649
|
+
}
|
|
650
|
+
if (!(el instanceof HTMLElement))
|
|
651
|
+
return el;
|
|
652
|
+
const baseText = textFor(el);
|
|
653
|
+
const popup = el.closest(payload.popupSelector);
|
|
654
|
+
let best = el;
|
|
655
|
+
let bestScore = Number.POSITIVE_INFINITY;
|
|
656
|
+
let current = el;
|
|
657
|
+
let depth = 0;
|
|
658
|
+
while (current && depth < 6) {
|
|
659
|
+
if (visible(current)) {
|
|
660
|
+
const rect = current.getBoundingClientRect();
|
|
661
|
+
const className = typeof current.className === 'string' ? current.className.toLowerCase() : '';
|
|
662
|
+
const role = current.getAttribute('role');
|
|
663
|
+
const tag = current.tagName.toLowerCase();
|
|
664
|
+
const currentText = textFor(current);
|
|
665
|
+
const rowLike = role === 'option' ||
|
|
666
|
+
role === 'menuitem' ||
|
|
667
|
+
role === 'treeitem' ||
|
|
668
|
+
tag === 'button' ||
|
|
669
|
+
tag === 'li' ||
|
|
670
|
+
tag === 'label' ||
|
|
671
|
+
current.hasAttribute('data-value') ||
|
|
672
|
+
current.hasAttribute('aria-selected') ||
|
|
673
|
+
current.hasAttribute('aria-checked') ||
|
|
674
|
+
className.includes('option') ||
|
|
675
|
+
className.includes('item') ||
|
|
676
|
+
className.includes('row') ||
|
|
677
|
+
className.includes('menu');
|
|
678
|
+
const textAligned = !!baseText && !!currentText && (currentText === baseText || currentText.includes(baseText));
|
|
679
|
+
const insidePopup = popup ? popup.contains(current) : !!current.closest(payload.popupSelector);
|
|
680
|
+
if ((rowLike || textAligned) && insidePopup) {
|
|
681
|
+
const score = rect.width * rect.height +
|
|
682
|
+
depth * 600 -
|
|
683
|
+
(rowLike ? 20_000 : 0) -
|
|
684
|
+
(textAligned ? 8_000 : 0);
|
|
685
|
+
if (score < bestScore) {
|
|
686
|
+
best = current;
|
|
687
|
+
bestScore = score;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
current = current.parentElement;
|
|
692
|
+
depth++;
|
|
693
|
+
}
|
|
694
|
+
return best;
|
|
695
|
+
}, { popupSelector: POPUP_CONTAINER_SELECTOR }));
|
|
696
|
+
return targetHandle;
|
|
697
|
+
}
|
|
698
|
+
async function collectVisibleOptionHints(page, anchor) {
|
|
699
|
+
const merged = new Map();
|
|
700
|
+
let hasPopup = false;
|
|
701
|
+
for (const frame of page.frames()) {
|
|
702
|
+
const snapshot = await frame.evaluate((payload) => {
|
|
703
|
+
function normalize(value) {
|
|
704
|
+
return value
|
|
705
|
+
.normalize('NFKD')
|
|
706
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
707
|
+
.replace(/[+﹢∔]/g, '+')
|
|
708
|
+
.replace(/[‐‑‒–—―]/g, '-')
|
|
709
|
+
.replace(/&/g, ' and ')
|
|
710
|
+
.replace(/\bplus\b/g, '+')
|
|
711
|
+
.replace(/[,/()]+/g, ' ')
|
|
712
|
+
.replace(/\s*\+\s*/g, '+')
|
|
713
|
+
.replace(/\s+/g, ' ')
|
|
714
|
+
.trim()
|
|
715
|
+
.toLowerCase();
|
|
716
|
+
}
|
|
717
|
+
function visible(el) {
|
|
718
|
+
if (!(el instanceof HTMLElement))
|
|
719
|
+
return false;
|
|
720
|
+
const rect = el.getBoundingClientRect();
|
|
721
|
+
if (rect.width <= 0 || rect.height <= 0)
|
|
722
|
+
return false;
|
|
723
|
+
const style = getComputedStyle(el);
|
|
724
|
+
return style.display !== 'none' && style.visibility !== 'hidden';
|
|
725
|
+
}
|
|
726
|
+
function labelFor(el) {
|
|
727
|
+
return el.getAttribute('aria-label')?.trim() || el.textContent?.trim() || '';
|
|
728
|
+
}
|
|
729
|
+
function selected(el) {
|
|
730
|
+
return (el.getAttribute('aria-selected') === 'true' ||
|
|
731
|
+
el.getAttribute('aria-checked') === 'true' ||
|
|
732
|
+
el.getAttribute('data-selected') === 'true' ||
|
|
733
|
+
el.getAttribute('data-state') === 'checked' ||
|
|
734
|
+
el.getAttribute('data-state') === 'on');
|
|
735
|
+
}
|
|
736
|
+
function highlighted(el) {
|
|
737
|
+
return (el === document.activeElement ||
|
|
738
|
+
el.getAttribute('data-highlighted') === 'true' ||
|
|
739
|
+
el.getAttribute('data-focus') === 'true' ||
|
|
740
|
+
el.getAttribute('data-focused') === 'true' ||
|
|
741
|
+
el.getAttribute('data-hovered') === 'true' ||
|
|
742
|
+
el.getAttribute('data-state') === 'active' ||
|
|
743
|
+
el.getAttribute('aria-current') === 'true');
|
|
744
|
+
}
|
|
745
|
+
const popupRoots = Array.from(document.querySelectorAll(payload.popupSelector)).filter((el) => visible(el));
|
|
746
|
+
const rows = [];
|
|
747
|
+
const seen = new Set();
|
|
748
|
+
for (const popup of popupRoots) {
|
|
749
|
+
const candidates = [popup, ...Array.from(popup.querySelectorAll(payload.optionSelector))];
|
|
750
|
+
for (const el of candidates) {
|
|
751
|
+
if (!(el instanceof Element) || !visible(el))
|
|
752
|
+
continue;
|
|
753
|
+
const className = typeof el.className === 'string' ? el.className.toLowerCase() : '';
|
|
754
|
+
const role = el.getAttribute('role');
|
|
755
|
+
const tag = el.tagName.toLowerCase();
|
|
756
|
+
const optionLike = role === 'option' ||
|
|
757
|
+
role === 'menuitem' ||
|
|
758
|
+
role === 'treeitem' ||
|
|
759
|
+
tag === 'button' ||
|
|
760
|
+
tag === 'li' ||
|
|
761
|
+
tag === 'label' ||
|
|
762
|
+
el.hasAttribute('data-value') ||
|
|
763
|
+
el.hasAttribute('aria-selected') ||
|
|
764
|
+
el.hasAttribute('aria-checked') ||
|
|
765
|
+
className.includes('option') ||
|
|
766
|
+
className.includes('item') ||
|
|
767
|
+
className.includes('row') ||
|
|
768
|
+
className.includes('menu');
|
|
769
|
+
const label = labelFor(el);
|
|
770
|
+
if (!label || label.length > 180)
|
|
771
|
+
continue;
|
|
772
|
+
if (!optionLike && !popup.contains(el.parentElement))
|
|
773
|
+
continue;
|
|
774
|
+
const key = normalize(label);
|
|
775
|
+
if (!key || seen.has(key))
|
|
776
|
+
continue;
|
|
777
|
+
const rect = el.getBoundingClientRect();
|
|
778
|
+
const centerX = rect.left + rect.width / 2;
|
|
779
|
+
const centerY = rect.top + rect.height / 2;
|
|
780
|
+
const distance = payload.anchorX === null && payload.anchorY === null
|
|
781
|
+
? rect.top
|
|
782
|
+
: Math.abs(centerX - (payload.anchorX ?? centerX)) + Math.abs(centerY - (payload.anchorY ?? centerY));
|
|
783
|
+
rows.push({
|
|
784
|
+
label,
|
|
785
|
+
selected: selected(el),
|
|
786
|
+
highlighted: highlighted(el),
|
|
787
|
+
rank: (selected(el) ? -4_000 : 0) + (highlighted(el) ? -2_000 : 0) + distance,
|
|
788
|
+
});
|
|
789
|
+
seen.add(key);
|
|
790
|
+
if (rows.length >= payload.maxOptions * 3)
|
|
791
|
+
break;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
rows.sort((a, b) => a.rank - b.rank || a.label.localeCompare(b.label));
|
|
795
|
+
return {
|
|
796
|
+
hasPopup: popupRoots.length > 0,
|
|
797
|
+
options: rows.slice(0, payload.maxOptions),
|
|
798
|
+
};
|
|
799
|
+
}, {
|
|
800
|
+
popupSelector: POPUP_ROOT_SELECTOR,
|
|
801
|
+
optionSelector: OPTION_PICKER_SELECTOR,
|
|
802
|
+
anchorX: anchor?.x ?? null,
|
|
803
|
+
anchorY: anchor?.y ?? null,
|
|
804
|
+
maxOptions: MAX_VISIBLE_OPTION_HINTS,
|
|
805
|
+
});
|
|
806
|
+
hasPopup ||= snapshot.hasPopup;
|
|
807
|
+
for (const option of snapshot.options) {
|
|
808
|
+
const key = normalizedOptionLabel(option.label);
|
|
809
|
+
const existing = merged.get(key);
|
|
810
|
+
const rank = (option.selected ? 0 : 10) + (option.highlighted ? 0 : 5) + merged.size;
|
|
811
|
+
if (!existing || rank < existing.rank) {
|
|
812
|
+
merged.set(key, { ...option, rank });
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
const options = [...merged.values()]
|
|
817
|
+
.sort((a, b) => a.rank - b.rank || a.label.localeCompare(b.label))
|
|
818
|
+
.slice(0, MAX_VISIBLE_OPTION_HINTS)
|
|
819
|
+
.map(({ label, selected, highlighted }) => ({ label, selected, highlighted }));
|
|
820
|
+
return { hasPopup, options };
|
|
821
|
+
}
|
|
822
|
+
async function activeListboxOptionLabel(page, anchor) {
|
|
823
|
+
for (const frame of page.frames()) {
|
|
824
|
+
const label = await frame.evaluate((payload) => {
|
|
825
|
+
function visible(el) {
|
|
826
|
+
if (!(el instanceof HTMLElement))
|
|
827
|
+
return false;
|
|
828
|
+
const rect = el.getBoundingClientRect();
|
|
829
|
+
if (rect.width <= 0 || rect.height <= 0)
|
|
830
|
+
return false;
|
|
831
|
+
const style = getComputedStyle(el);
|
|
832
|
+
return style.display !== 'none' && style.visibility !== 'hidden';
|
|
833
|
+
}
|
|
834
|
+
function labelFor(el) {
|
|
835
|
+
const text = el?.getAttribute('aria-label')?.trim() || el?.textContent?.trim() || '';
|
|
836
|
+
return text || null;
|
|
837
|
+
}
|
|
838
|
+
function highlighted(el) {
|
|
839
|
+
return (el === document.activeElement ||
|
|
840
|
+
el.getAttribute('data-highlighted') === 'true' ||
|
|
841
|
+
el.getAttribute('data-focus') === 'true' ||
|
|
842
|
+
el.getAttribute('data-focused') === 'true' ||
|
|
843
|
+
el.getAttribute('data-hovered') === 'true' ||
|
|
844
|
+
el.getAttribute('data-state') === 'active' ||
|
|
845
|
+
el.getAttribute('aria-current') === 'true' ||
|
|
846
|
+
el.getAttribute('aria-selected') === 'true');
|
|
847
|
+
}
|
|
848
|
+
const active = document.activeElement;
|
|
849
|
+
const activeDescendantId = active?.getAttribute('aria-activedescendant');
|
|
850
|
+
const referenced = activeDescendantId ? document.getElementById(activeDescendantId) : null;
|
|
851
|
+
if (referenced && visible(referenced))
|
|
852
|
+
return labelFor(referenced);
|
|
853
|
+
const candidates = Array.from(document.querySelectorAll(payload.optionSelector)).filter(el => visible(el) && highlighted(el));
|
|
854
|
+
let best = null;
|
|
855
|
+
for (const el of candidates) {
|
|
856
|
+
const label = labelFor(el);
|
|
857
|
+
if (!label)
|
|
858
|
+
continue;
|
|
859
|
+
const rect = el.getBoundingClientRect();
|
|
860
|
+
const centerX = rect.left + rect.width / 2;
|
|
861
|
+
const centerY = rect.top + rect.height / 2;
|
|
862
|
+
const distance = payload.anchorX === null && payload.anchorY === null
|
|
863
|
+
? rect.top
|
|
864
|
+
: Math.abs(centerX - (payload.anchorX ?? centerX)) + Math.abs(centerY - (payload.anchorY ?? centerY));
|
|
865
|
+
if (!best || distance < best.score)
|
|
866
|
+
best = { label, score: distance };
|
|
867
|
+
}
|
|
868
|
+
return best?.label ?? null;
|
|
869
|
+
}, {
|
|
870
|
+
optionSelector: OPTION_PICKER_SELECTOR,
|
|
871
|
+
anchorX: anchor?.x ?? null,
|
|
872
|
+
anchorY: anchor?.y ?? null,
|
|
873
|
+
});
|
|
874
|
+
if (label)
|
|
875
|
+
return label;
|
|
876
|
+
}
|
|
877
|
+
return null;
|
|
878
|
+
}
|
|
879
|
+
async function tryKeyboardSelectVisibleOption(page, label, exact, anchor, focusLocator) {
|
|
880
|
+
if (focusLocator) {
|
|
881
|
+
try {
|
|
882
|
+
await focusLocator.click();
|
|
883
|
+
}
|
|
884
|
+
catch {
|
|
885
|
+
/* ignore */
|
|
886
|
+
}
|
|
887
|
+
try {
|
|
888
|
+
await focusLocator.focus();
|
|
889
|
+
}
|
|
890
|
+
catch {
|
|
891
|
+
/* ignore */
|
|
892
|
+
}
|
|
893
|
+
await delay(40);
|
|
894
|
+
}
|
|
895
|
+
const visible = await collectVisibleOptionHints(page, anchor);
|
|
896
|
+
if (!visible.options.some(option => selectionMatchScore(option.label, label, exact) !== null)) {
|
|
897
|
+
return null;
|
|
898
|
+
}
|
|
899
|
+
for (const key of ['ArrowDown', 'ArrowUp']) {
|
|
900
|
+
const seen = new Set();
|
|
901
|
+
for (let step = 0; step < LISTBOX_KEYBOARD_FALLBACK_STEPS; step++) {
|
|
902
|
+
await page.keyboard.press(key);
|
|
903
|
+
await delay(50);
|
|
904
|
+
const active = await activeListboxOptionLabel(page, anchor);
|
|
905
|
+
if (!active)
|
|
906
|
+
continue;
|
|
907
|
+
if (selectionMatchScore(active, label, exact) !== null) {
|
|
908
|
+
await page.keyboard.press('Enter');
|
|
909
|
+
return active;
|
|
910
|
+
}
|
|
911
|
+
const normalized = normalizedOptionLabel(active);
|
|
912
|
+
if (seen.has(normalized))
|
|
913
|
+
break;
|
|
914
|
+
seen.add(normalized);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
return null;
|
|
918
|
+
}
|
|
919
|
+
function listboxErrorMessage(opts) {
|
|
920
|
+
const visibleOptions = (opts.visibleOptions ?? []).map(option => option.label).slice(0, MAX_VISIBLE_OPTION_HINTS);
|
|
921
|
+
const payload = {
|
|
922
|
+
error: 'listboxPick',
|
|
923
|
+
reason: opts.reason,
|
|
924
|
+
message: opts.reason === 'field_not_found'
|
|
925
|
+
? `listboxPick: no visible combobox/dropdown matching field "${opts.fieldLabel ?? 'unknown'}"`
|
|
926
|
+
: opts.reason === 'selection_not_confirmed'
|
|
927
|
+
? `listboxPick: selected "${opts.requestedLabel}" but could not confirm it on field "${opts.fieldLabel ?? 'unknown'}"`
|
|
928
|
+
: `listboxPick: no visible option matching "${opts.requestedLabel}"`,
|
|
929
|
+
requestedLabel: opts.requestedLabel,
|
|
930
|
+
...(opts.fieldLabel ? { fieldLabel: opts.fieldLabel } : {}),
|
|
931
|
+
...(opts.query ? { query: opts.query } : {}),
|
|
932
|
+
exact: opts.exact,
|
|
933
|
+
...(opts.listEmpty !== undefined ? { listEmpty: opts.listEmpty } : {}),
|
|
934
|
+
...(opts.queryReset ? { queryReset: true } : {}),
|
|
935
|
+
visibleOptionCount: visibleOptions.length,
|
|
936
|
+
visibleOptions,
|
|
937
|
+
suggestedAction: visibleOptions.length > 0
|
|
938
|
+
? 'Retry with one of visibleOptions, or pass a shorter query/alias for searchable comboboxes.'
|
|
939
|
+
: opts.listEmpty
|
|
940
|
+
? 'The list appears empty. Retry after clearing the search query or reopening the dropdown.'
|
|
941
|
+
: 'Open the dropdown first, or retry with fieldLabel so Geometra can anchor to the correct combobox.',
|
|
942
|
+
};
|
|
943
|
+
return JSON.stringify(payload, null, 2);
|
|
944
|
+
}
|
|
515
945
|
async function clickVisibleOptionCandidate(page, label, exact, anchor) {
|
|
516
946
|
for (const frame of page.frames()) {
|
|
517
947
|
const candidates = frame.locator(OPTION_PICKER_SELECTOR);
|
|
@@ -520,7 +950,18 @@ async function clickVisibleOptionCandidate(page, label, exact, anchor) {
|
|
|
520
950
|
continue;
|
|
521
951
|
const bestIndex = await candidates.evaluateAll((elements, payload) => {
|
|
522
952
|
function normalize(value) {
|
|
523
|
-
return value
|
|
953
|
+
return value
|
|
954
|
+
.normalize('NFKD')
|
|
955
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
956
|
+
.replace(/[+﹢∔]/g, '+')
|
|
957
|
+
.replace(/[‐‑‒–—―]/g, '-')
|
|
958
|
+
.replace(/&/g, ' and ')
|
|
959
|
+
.replace(/\bplus\b/g, '+')
|
|
960
|
+
.replace(/[,/()]+/g, ' ')
|
|
961
|
+
.replace(/\s*\+\s*/g, '+')
|
|
962
|
+
.replace(/\s+/g, ' ')
|
|
963
|
+
.trim()
|
|
964
|
+
.toLowerCase();
|
|
524
965
|
}
|
|
525
966
|
function aliases(value) {
|
|
526
967
|
const out = new Set([value]);
|
|
@@ -539,6 +980,30 @@ async function clickVisibleOptionCandidate(page, label, exact, anchor) {
|
|
|
539
980
|
out.add(alias);
|
|
540
981
|
}
|
|
541
982
|
}
|
|
983
|
+
if (value === 'atx' || value.includes('austin')) {
|
|
984
|
+
for (const alias of ['atx', 'austin', 'austin tx', 'austin texas'])
|
|
985
|
+
out.add(alias);
|
|
986
|
+
}
|
|
987
|
+
if (value === 'nyc' || value.includes('new york')) {
|
|
988
|
+
for (const alias of ['nyc', 'new york', 'new york ny'])
|
|
989
|
+
out.add(alias);
|
|
990
|
+
}
|
|
991
|
+
if (value === 'sf' || value.includes('san francisco')) {
|
|
992
|
+
for (const alias of ['sf', 'san francisco', 'san francisco ca'])
|
|
993
|
+
out.add(alias);
|
|
994
|
+
}
|
|
995
|
+
if (value === 'la' || value.includes('los angeles')) {
|
|
996
|
+
for (const alias of ['la', 'los angeles', 'los angeles ca'])
|
|
997
|
+
out.add(alias);
|
|
998
|
+
}
|
|
999
|
+
if (value === 'dc' || value.includes('washington dc')) {
|
|
1000
|
+
for (const alias of ['dc', 'washington dc', 'washington d c'])
|
|
1001
|
+
out.add(alias);
|
|
1002
|
+
}
|
|
1003
|
+
if (value === 'us' || value === 'usa' || value.includes('united states')) {
|
|
1004
|
+
for (const alias of ['us', 'usa', 'united states'])
|
|
1005
|
+
out.add(alias);
|
|
1006
|
+
}
|
|
542
1007
|
return [...out];
|
|
543
1008
|
}
|
|
544
1009
|
function hasNegativeCue(value) {
|
|
@@ -590,7 +1055,7 @@ async function clickVisibleOptionCandidate(page, label, exact, anchor) {
|
|
|
590
1055
|
return style.display !== 'none' && style.visibility !== 'hidden';
|
|
591
1056
|
}
|
|
592
1057
|
function popupWeight(el) {
|
|
593
|
-
return el.closest(
|
|
1058
|
+
return el.closest(payload.popupSelector)
|
|
594
1059
|
? 0
|
|
595
1060
|
: 220;
|
|
596
1061
|
}
|
|
@@ -618,13 +1083,24 @@ async function clickVisibleOptionCandidate(page, label, exact, anchor) {
|
|
|
618
1083
|
best = { index: i, score };
|
|
619
1084
|
}
|
|
620
1085
|
return best?.index ?? -1;
|
|
621
|
-
}, {
|
|
1086
|
+
}, {
|
|
1087
|
+
label,
|
|
1088
|
+
exact,
|
|
1089
|
+
anchorX: anchor?.x ?? null,
|
|
1090
|
+
anchorY: anchor?.y ?? null,
|
|
1091
|
+
popupSelector: POPUP_CONTAINER_SELECTOR,
|
|
1092
|
+
});
|
|
622
1093
|
if (bestIndex >= 0) {
|
|
623
|
-
const
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
1094
|
+
const candidate = candidates.nth(bestIndex);
|
|
1095
|
+
const selectedText = (await candidate.evaluate(el => el.getAttribute('aria-label')?.trim() || el.textContent?.trim() || '').catch(() => '')) || null;
|
|
1096
|
+
const clickTarget = await resolveMeaningfulOptionClickTarget(candidate);
|
|
1097
|
+
if (clickTarget) {
|
|
1098
|
+
await clickTarget.scrollIntoViewIfNeeded();
|
|
1099
|
+
await clickTarget.click();
|
|
1100
|
+
}
|
|
1101
|
+
else {
|
|
1102
|
+
await candidate.click();
|
|
1103
|
+
}
|
|
628
1104
|
return selectedText;
|
|
629
1105
|
}
|
|
630
1106
|
}
|
|
@@ -654,7 +1130,18 @@ async function visibleOptionIsSelected(page, label, exact, anchor) {
|
|
|
654
1130
|
continue;
|
|
655
1131
|
const selected = await candidates.evaluateAll((elements, payload) => {
|
|
656
1132
|
function normalize(value) {
|
|
657
|
-
return value
|
|
1133
|
+
return value
|
|
1134
|
+
.normalize('NFKD')
|
|
1135
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
1136
|
+
.replace(/[+﹢∔]/g, '+')
|
|
1137
|
+
.replace(/[‐‑‒–—―]/g, '-')
|
|
1138
|
+
.replace(/&/g, ' and ')
|
|
1139
|
+
.replace(/\bplus\b/g, '+')
|
|
1140
|
+
.replace(/[,/()]+/g, ' ')
|
|
1141
|
+
.replace(/\s*\+\s*/g, '+')
|
|
1142
|
+
.replace(/\s+/g, ' ')
|
|
1143
|
+
.trim()
|
|
1144
|
+
.toLowerCase();
|
|
658
1145
|
}
|
|
659
1146
|
function aliases(value) {
|
|
660
1147
|
const out = new Set([value]);
|
|
@@ -673,6 +1160,30 @@ async function visibleOptionIsSelected(page, label, exact, anchor) {
|
|
|
673
1160
|
out.add(alias);
|
|
674
1161
|
}
|
|
675
1162
|
}
|
|
1163
|
+
if (value === 'atx' || value.includes('austin')) {
|
|
1164
|
+
for (const alias of ['atx', 'austin', 'austin tx', 'austin texas'])
|
|
1165
|
+
out.add(alias);
|
|
1166
|
+
}
|
|
1167
|
+
if (value === 'nyc' || value.includes('new york')) {
|
|
1168
|
+
for (const alias of ['nyc', 'new york', 'new york ny'])
|
|
1169
|
+
out.add(alias);
|
|
1170
|
+
}
|
|
1171
|
+
if (value === 'sf' || value.includes('san francisco')) {
|
|
1172
|
+
for (const alias of ['sf', 'san francisco', 'san francisco ca'])
|
|
1173
|
+
out.add(alias);
|
|
1174
|
+
}
|
|
1175
|
+
if (value === 'la' || value.includes('los angeles')) {
|
|
1176
|
+
for (const alias of ['la', 'los angeles', 'los angeles ca'])
|
|
1177
|
+
out.add(alias);
|
|
1178
|
+
}
|
|
1179
|
+
if (value === 'dc' || value.includes('washington dc')) {
|
|
1180
|
+
for (const alias of ['dc', 'washington dc', 'washington d c'])
|
|
1181
|
+
out.add(alias);
|
|
1182
|
+
}
|
|
1183
|
+
if (value === 'us' || value === 'usa' || value.includes('united states')) {
|
|
1184
|
+
for (const alias of ['us', 'usa', 'united states'])
|
|
1185
|
+
out.add(alias);
|
|
1186
|
+
}
|
|
676
1187
|
return [...out];
|
|
677
1188
|
}
|
|
678
1189
|
function hasNegativeCue(value) {
|
|
@@ -750,10 +1261,19 @@ async function visibleOptionIsSelected(page, label, exact, anchor) {
|
|
|
750
1261
|
}
|
|
751
1262
|
return false;
|
|
752
1263
|
}
|
|
753
|
-
async function confirmListboxSelection(page, fieldLabel, label, exact, anchor, currentHandle, selectedOptionText) {
|
|
1264
|
+
async function confirmListboxSelection(page, fieldLabel, label, exact, anchor, currentHandle, selectedOptionText, opts) {
|
|
1265
|
+
const canTrustEditableDisplayMatch = async () => {
|
|
1266
|
+
if (!opts?.editable)
|
|
1267
|
+
return true;
|
|
1268
|
+
if (await visibleOptionIsSelected(page, label, exact, anchor))
|
|
1269
|
+
return true;
|
|
1270
|
+
const popupState = await collectVisibleOptionHints(page, anchor);
|
|
1271
|
+
return !popupState.hasPopup;
|
|
1272
|
+
};
|
|
754
1273
|
if (currentHandle) {
|
|
755
1274
|
const immediateValues = await elementHandleDisplayedValues(currentHandle);
|
|
756
|
-
if (immediateValues.some(value => displayedValueMatchesSelection(value, label, exact, selectedOptionText))
|
|
1275
|
+
if (immediateValues.some(value => displayedValueMatchesSelection(value, label, exact, selectedOptionText)) &&
|
|
1276
|
+
await canTrustEditableDisplayMatch()) {
|
|
757
1277
|
return true;
|
|
758
1278
|
}
|
|
759
1279
|
}
|
|
@@ -764,8 +1284,10 @@ async function confirmListboxSelection(page, fieldLabel, label, exact, anchor, c
|
|
|
764
1284
|
if (!locator)
|
|
765
1285
|
continue;
|
|
766
1286
|
const values = await locatorDisplayedValues(locator);
|
|
767
|
-
if (values.some(value => displayedValueMatchesSelection(value, label, exact, selectedOptionText))
|
|
1287
|
+
if (values.some(value => displayedValueMatchesSelection(value, label, exact, selectedOptionText)) &&
|
|
1288
|
+
await canTrustEditableDisplayMatch()) {
|
|
768
1289
|
return true;
|
|
1290
|
+
}
|
|
769
1291
|
}
|
|
770
1292
|
if (await visibleOptionIsSelected(page, label, exact, anchor))
|
|
771
1293
|
return true;
|
|
@@ -847,6 +1369,14 @@ async function findLabeledFileInput(frame, fieldLabel, exact) {
|
|
|
847
1369
|
if (text)
|
|
848
1370
|
return text;
|
|
849
1371
|
}
|
|
1372
|
+
if (el.parentElement?.tagName.toLowerCase() === 'label') {
|
|
1373
|
+
const text = el.parentElement.textContent?.trim();
|
|
1374
|
+
if (text)
|
|
1375
|
+
return text;
|
|
1376
|
+
}
|
|
1377
|
+
const title = el.getAttribute('title')?.trim();
|
|
1378
|
+
if (title)
|
|
1379
|
+
return title;
|
|
850
1380
|
return undefined;
|
|
851
1381
|
}
|
|
852
1382
|
for (let i = 0; i < elements.length; i++) {
|
|
@@ -998,6 +1528,7 @@ async function findLabeledEditableField(page, fieldLabel, exact, cache, fieldId)
|
|
|
998
1528
|
for (const frame of page.frames()) {
|
|
999
1529
|
const candidates = [
|
|
1000
1530
|
frame.getByLabel(fieldLabel, { exact }),
|
|
1531
|
+
frame.getByPlaceholder(fieldLabel, { exact }),
|
|
1001
1532
|
frame.getByRole('textbox', { name: fieldLabel, exact }),
|
|
1002
1533
|
frame.getByRole('combobox', { name: fieldLabel, exact }),
|
|
1003
1534
|
];
|
|
@@ -1205,6 +1736,20 @@ async function attemptNativeBatchFill(page, fields) {
|
|
|
1205
1736
|
if (el.parentElement?.tagName.toLowerCase() === 'label') {
|
|
1206
1737
|
return el.parentElement.textContent?.trim() || undefined;
|
|
1207
1738
|
}
|
|
1739
|
+
if ((el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) &&
|
|
1740
|
+
!['checkbox', 'radio', 'file', 'button', 'submit', 'reset', 'hidden'].includes(el instanceof HTMLInputElement ? el.type : '')) {
|
|
1741
|
+
const placeholder = el.getAttribute('aria-placeholder')?.trim() || el.getAttribute('placeholder')?.trim();
|
|
1742
|
+
if (placeholder)
|
|
1743
|
+
return placeholder;
|
|
1744
|
+
}
|
|
1745
|
+
if (el instanceof HTMLInputElement && ['button', 'submit', 'reset'].includes(el.type)) {
|
|
1746
|
+
const value = el.value?.trim();
|
|
1747
|
+
if (value)
|
|
1748
|
+
return value;
|
|
1749
|
+
}
|
|
1750
|
+
const title = el.getAttribute('title')?.trim();
|
|
1751
|
+
if (title)
|
|
1752
|
+
return title;
|
|
1208
1753
|
return undefined;
|
|
1209
1754
|
}
|
|
1210
1755
|
function dispatch(target) {
|
|
@@ -1524,6 +2069,23 @@ async function chooseValueFromLabeledGroup(page, fieldLabel, value, exact) {
|
|
|
1524
2069
|
if (el instanceof HTMLInputElement && el.labels && el.labels.length > 0) {
|
|
1525
2070
|
return el.labels[0]?.textContent?.trim() || undefined;
|
|
1526
2071
|
}
|
|
2072
|
+
if (el.parentElement?.tagName.toLowerCase() === 'label') {
|
|
2073
|
+
return el.parentElement.textContent?.trim() || undefined;
|
|
2074
|
+
}
|
|
2075
|
+
if ((el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) &&
|
|
2076
|
+
!['checkbox', 'radio', 'file', 'button', 'submit', 'reset', 'hidden'].includes(el instanceof HTMLInputElement ? el.type : '')) {
|
|
2077
|
+
const placeholder = el.getAttribute('aria-placeholder')?.trim() || el.getAttribute('placeholder')?.trim();
|
|
2078
|
+
if (placeholder)
|
|
2079
|
+
return placeholder;
|
|
2080
|
+
}
|
|
2081
|
+
if (el instanceof HTMLInputElement && ['button', 'submit', 'reset'].includes(el.type)) {
|
|
2082
|
+
const value = el.value?.trim();
|
|
2083
|
+
if (value)
|
|
2084
|
+
return value;
|
|
2085
|
+
}
|
|
2086
|
+
const title = el.getAttribute('title')?.trim();
|
|
2087
|
+
if (title)
|
|
2088
|
+
return title;
|
|
1527
2089
|
return undefined;
|
|
1528
2090
|
}
|
|
1529
2091
|
function choiceLabel(el) {
|
|
@@ -1806,16 +2368,34 @@ export async function pickListboxOption(page, label, opts) {
|
|
|
1806
2368
|
let attemptedSelection = false;
|
|
1807
2369
|
let selectedOptionText;
|
|
1808
2370
|
let openedHandle;
|
|
2371
|
+
let openedLocator;
|
|
2372
|
+
let openedEditable = false;
|
|
2373
|
+
let queryUsed;
|
|
2374
|
+
let queryReset = false;
|
|
1809
2375
|
if (opts?.fieldLabel) {
|
|
1810
|
-
|
|
2376
|
+
let opened;
|
|
2377
|
+
try {
|
|
2378
|
+
opened = await openDropdownControl(page, opts.fieldLabel, exact, opts.cache, opts.fieldId);
|
|
2379
|
+
}
|
|
2380
|
+
catch {
|
|
2381
|
+
throw new Error(listboxErrorMessage({
|
|
2382
|
+
reason: 'field_not_found',
|
|
2383
|
+
requestedLabel: label,
|
|
2384
|
+
fieldLabel: opts.fieldLabel,
|
|
2385
|
+
query: opts?.query,
|
|
2386
|
+
exact,
|
|
2387
|
+
}));
|
|
2388
|
+
}
|
|
1811
2389
|
anchor = { x: opened.anchorX, y: opened.anchorY };
|
|
1812
2390
|
openedHandle = opened.handle;
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
2391
|
+
openedLocator = opened.locator;
|
|
2392
|
+
openedEditable = opened.editable;
|
|
2393
|
+
queryUsed = opts.query ?? label;
|
|
2394
|
+
if (queryUsed && opened.editable) {
|
|
2395
|
+
await typeIntoEditableLocator(page, opened.locator, queryUsed);
|
|
1816
2396
|
await delay(80);
|
|
1817
2397
|
}
|
|
1818
|
-
else if (
|
|
2398
|
+
else if (queryUsed && await typeIntoActiveEditableElement(page, queryUsed)) {
|
|
1819
2399
|
await delay(80);
|
|
1820
2400
|
}
|
|
1821
2401
|
}
|
|
@@ -1824,22 +2404,66 @@ export async function pickListboxOption(page, label, opts) {
|
|
|
1824
2404
|
anchor = { x: opts.openX, y: opts.openY };
|
|
1825
2405
|
await delay(120);
|
|
1826
2406
|
}
|
|
1827
|
-
const
|
|
1828
|
-
while (Date.now() < deadline) {
|
|
2407
|
+
const attemptClickSelection = async () => {
|
|
1829
2408
|
selectedOptionText = (await clickVisibleOptionCandidate(page, label, exact, anchor)) ?? undefined;
|
|
1830
|
-
if (selectedOptionText)
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
2409
|
+
if (!selectedOptionText)
|
|
2410
|
+
return false;
|
|
2411
|
+
attemptedSelection = true;
|
|
2412
|
+
if (!opts?.fieldLabel ||
|
|
2413
|
+
await confirmListboxSelection(page, opts.fieldLabel, label, exact, anchor, openedHandle, selectedOptionText, {
|
|
2414
|
+
editable: openedEditable,
|
|
2415
|
+
})) {
|
|
2416
|
+
return true;
|
|
2417
|
+
}
|
|
2418
|
+
return false;
|
|
2419
|
+
};
|
|
2420
|
+
if (await attemptClickSelection())
|
|
2421
|
+
return;
|
|
2422
|
+
let visibleHints = await collectVisibleOptionHints(page, anchor);
|
|
2423
|
+
const visibleMatchExists = visibleHints.options.some(option => selectionMatchScore(option.label, label, exact) !== null);
|
|
2424
|
+
if (queryUsed && !visibleMatchExists) {
|
|
2425
|
+
queryReset = await resetTypedListboxQuery(page, openedLocator);
|
|
2426
|
+
if (queryReset) {
|
|
2427
|
+
await delay(80);
|
|
2428
|
+
if (await attemptClickSelection())
|
|
1834
2429
|
return;
|
|
1835
|
-
|
|
2430
|
+
visibleHints = await collectVisibleOptionHints(page, anchor);
|
|
1836
2431
|
}
|
|
1837
|
-
await delay(120);
|
|
1838
2432
|
}
|
|
1839
|
-
|
|
1840
|
-
|
|
2433
|
+
const keyboardSelection = await tryKeyboardSelectVisibleOption(page, label, exact, anchor, openedLocator);
|
|
2434
|
+
if (keyboardSelection) {
|
|
2435
|
+
selectedOptionText = keyboardSelection;
|
|
2436
|
+
attemptedSelection = true;
|
|
2437
|
+
if (!opts?.fieldLabel ||
|
|
2438
|
+
await confirmListboxSelection(page, opts.fieldLabel, label, exact, anchor, openedHandle, selectedOptionText, {
|
|
2439
|
+
editable: openedEditable,
|
|
2440
|
+
})) {
|
|
2441
|
+
return;
|
|
2442
|
+
}
|
|
1841
2443
|
}
|
|
1842
|
-
|
|
2444
|
+
visibleHints = await collectVisibleOptionHints(page, anchor);
|
|
2445
|
+
if (opts?.fieldLabel && attemptedSelection) {
|
|
2446
|
+
throw new Error(listboxErrorMessage({
|
|
2447
|
+
reason: 'selection_not_confirmed',
|
|
2448
|
+
requestedLabel: label,
|
|
2449
|
+
fieldLabel: opts.fieldLabel,
|
|
2450
|
+
query: queryUsed,
|
|
2451
|
+
exact,
|
|
2452
|
+
visibleOptions: visibleHints.options,
|
|
2453
|
+
listEmpty: visibleHints.hasPopup && visibleHints.options.length === 0,
|
|
2454
|
+
queryReset,
|
|
2455
|
+
}));
|
|
2456
|
+
}
|
|
2457
|
+
throw new Error(listboxErrorMessage({
|
|
2458
|
+
reason: 'no_visible_option_match',
|
|
2459
|
+
requestedLabel: label,
|
|
2460
|
+
fieldLabel: opts?.fieldLabel,
|
|
2461
|
+
query: queryUsed,
|
|
2462
|
+
exact,
|
|
2463
|
+
visibleOptions: visibleHints.options,
|
|
2464
|
+
listEmpty: visibleHints.hasPopup && visibleHints.options.length === 0,
|
|
2465
|
+
queryReset,
|
|
2466
|
+
}));
|
|
1843
2467
|
}
|
|
1844
2468
|
/**
|
|
1845
2469
|
* Set a checkbox/radio by accessible label instead of brittle coordinate clicks.
|