@geometra/proxy 1.37.0 → 1.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dom-actions.d.ts.map +1 -1
- package/dist/dom-actions.js +289 -24
- package/dist/dom-actions.js.map +1 -1
- package/dist/extractor.d.ts.map +1 -1
- package/dist/extractor.js +57 -2
- package/dist/extractor.js.map +1 -1
- package/dist/geometry-ws.d.ts.map +1 -1
- package/dist/geometry-ws.js +69 -11
- package/dist/geometry-ws.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dom-actions.d.ts","sourceRoot":"","sources":["../src/dom-actions.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAwB,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAA;AACrE,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAMnE,MAAM,MAAM,kBAAkB,GAAG,MAAM,GAAG,SAAS,GAAG,QAAQ,GAAG,MAAM,CAAA;AAqCvE,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAAC,CAAA;IACpC,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAAC,CAAA;IACrC,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAAC,CAAA;CACvC;AAED,wBAAgB,qBAAqB,IAAI,eAAe,CAMvD;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,eAAe,GAAG,IAAI,CAIjE;
|
|
1
|
+
{"version":3,"file":"dom-actions.d.ts","sourceRoot":"","sources":["../src/dom-actions.ts"],"names":[],"mappings":"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"}
|
package/dist/dom-actions.js
CHANGED
|
@@ -1553,6 +1553,155 @@ async function elementHandleDisplayedValues(handle) {
|
|
|
1553
1553
|
return [];
|
|
1554
1554
|
}
|
|
1555
1555
|
}
|
|
1556
|
+
/**
|
|
1557
|
+
* Read the authoritative `aria-invalid` state of a combobox trigger (or its
|
|
1558
|
+
* nearest owning combobox ancestor).
|
|
1559
|
+
*
|
|
1560
|
+
* This is the ONLY reliable signal for whether a custom listbox library
|
|
1561
|
+
* (react-select, Radix, Headless UI, Downshift, Workday PTX, Ashby, Lever
|
|
1562
|
+
* forms, etc.) considers a required field committed. Displayed-value reads
|
|
1563
|
+
* can return stale placeholder text or empty strings during closing
|
|
1564
|
+
* animations, and the library's internal React state may revert a silent
|
|
1565
|
+
* commit a few frames after an option click — but whenever that happens,
|
|
1566
|
+
* the library sets `aria-invalid="true"` back on the trigger element.
|
|
1567
|
+
*
|
|
1568
|
+
* Returns:
|
|
1569
|
+
* - `true` if the trigger (or an owning combobox ancestor) is explicitly
|
|
1570
|
+
* marked `aria-invalid="true"`.
|
|
1571
|
+
* - `false` if it is explicitly `"false"` or unset. Callers should treat
|
|
1572
|
+
* `false` as "no veto" — not as "definitely committed" — because some
|
|
1573
|
+
* libraries simply omit the attribute when valid.
|
|
1574
|
+
* - `false` on read errors (detached handles, cross-origin frames, etc.)
|
|
1575
|
+
* so verification proceeds on the other positive signals.
|
|
1576
|
+
*/
|
|
1577
|
+
async function readAriaInvalid(handle) {
|
|
1578
|
+
if (!handle)
|
|
1579
|
+
return false;
|
|
1580
|
+
try {
|
|
1581
|
+
return await handle.evaluate((el) => {
|
|
1582
|
+
if (!(el instanceof Element))
|
|
1583
|
+
return false;
|
|
1584
|
+
// Walk the element itself plus a few ancestors. react-select exposes the
|
|
1585
|
+
// hidden input as the focusable trigger; its aria-invalid is mirrored on
|
|
1586
|
+
// the combobox wrapper. Radix Select puts aria-invalid on the trigger
|
|
1587
|
+
// button. Covering both patterns with a short climb keeps this generic.
|
|
1588
|
+
let cursor = el;
|
|
1589
|
+
let depth = 0;
|
|
1590
|
+
while (cursor && depth < 4) {
|
|
1591
|
+
const attr = cursor.getAttribute('aria-invalid');
|
|
1592
|
+
if (attr !== null) {
|
|
1593
|
+
return attr === 'true' || attr === '';
|
|
1594
|
+
}
|
|
1595
|
+
cursor = cursor.parentElement;
|
|
1596
|
+
depth++;
|
|
1597
|
+
}
|
|
1598
|
+
return false;
|
|
1599
|
+
});
|
|
1600
|
+
}
|
|
1601
|
+
catch {
|
|
1602
|
+
return false;
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
/**
|
|
1606
|
+
* Read whether a combobox trigger is currently displaying a *placeholder*
|
|
1607
|
+
* (uncommitted) instead of a committed value.
|
|
1608
|
+
*
|
|
1609
|
+
* The aria-invalid veto in `confirmListboxSelection` /
|
|
1610
|
+
* `dismissAndReVerifySelection` / `pickListboxOption.postCommitVerify`
|
|
1611
|
+
* catches libraries that flip aria-invalid back to "true" after a failed
|
|
1612
|
+
* commit (Workday PTX, some Ashby flows, certain react-select forks). It does
|
|
1613
|
+
* NOT catch the more common Greenhouse / Lever pattern where the library
|
|
1614
|
+
* leaves the field at the "Select..." placeholder with aria-invalid simply
|
|
1615
|
+
* absent — that field is empty and required, but no library-side validation
|
|
1616
|
+
* has fired yet, so aria-invalid won't be set until Submit. The verification
|
|
1617
|
+
* pipeline still has to know the field is empty so it can refuse to claim a
|
|
1618
|
+
* silent success.
|
|
1619
|
+
*
|
|
1620
|
+
* The authoritative signal for this case is the trigger's *visible text*.
|
|
1621
|
+
* react-select renders `.select__placeholder` when empty and
|
|
1622
|
+
* `.select__single-value` when committed; Radix Select renders the
|
|
1623
|
+
* `<SelectValue placeholder="...">` text when empty and the option text when
|
|
1624
|
+
* committed; Lever / generic ARIA listboxes leave the trigger's textContent
|
|
1625
|
+
* matching the original "Select..." prompt until something is chosen. Looking
|
|
1626
|
+
* at the trigger's visible text and matching it against
|
|
1627
|
+
* PLACEHOLDER_PATTERN tells us reliably whether anything was actually
|
|
1628
|
+
* selected, regardless of which library is in play.
|
|
1629
|
+
*
|
|
1630
|
+
* Returns:
|
|
1631
|
+
* - `true` only if the trigger has visible text AND that text matches the
|
|
1632
|
+
* placeholder pattern. This is "definitely empty".
|
|
1633
|
+
* - `false` if the trigger shows text that does NOT match a placeholder, or
|
|
1634
|
+
* if no visible text could be read at all (read errors, empty trigger,
|
|
1635
|
+
* handle detached). Treat `false` as "no veto" — same convention as
|
|
1636
|
+
* readAriaInvalid — so verification keeps running on positive signals.
|
|
1637
|
+
*/
|
|
1638
|
+
async function readTriggerShowsPlaceholder(handle) {
|
|
1639
|
+
if (!handle)
|
|
1640
|
+
return false;
|
|
1641
|
+
try {
|
|
1642
|
+
const visibleText = await handle.evaluate((el) => {
|
|
1643
|
+
if (!(el instanceof HTMLElement))
|
|
1644
|
+
return null;
|
|
1645
|
+
function visible(node) {
|
|
1646
|
+
if (!(node instanceof HTMLElement))
|
|
1647
|
+
return false;
|
|
1648
|
+
const rect = node.getBoundingClientRect();
|
|
1649
|
+
if (rect.width <= 0 || rect.height <= 0)
|
|
1650
|
+
return false;
|
|
1651
|
+
const style = getComputedStyle(node);
|
|
1652
|
+
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0')
|
|
1653
|
+
return false;
|
|
1654
|
+
return true;
|
|
1655
|
+
}
|
|
1656
|
+
function readPresentational(root) {
|
|
1657
|
+
// react-select / Radix / Headless UI all render the "currently
|
|
1658
|
+
// displayed value" inside a leaf-ish presentational element near the
|
|
1659
|
+
// trigger. Walk a few ancestors looking for either the placeholder
|
|
1660
|
+
// class or the single-value class. The first match wins because the
|
|
1661
|
+
// library renders one or the other, never both.
|
|
1662
|
+
let cur = root;
|
|
1663
|
+
let depth = 0;
|
|
1664
|
+
while (cur && depth < 4) {
|
|
1665
|
+
const candidates = cur.querySelectorAll('[class*="placeholder"], [class*="Placeholder"], [class*="single-value"], [class*="singleValue"], [class*="SelectValue"], [data-radix-select-value]');
|
|
1666
|
+
for (const candidate of candidates) {
|
|
1667
|
+
if (!visible(candidate))
|
|
1668
|
+
continue;
|
|
1669
|
+
if (candidate.getAttribute('aria-hidden') === 'true')
|
|
1670
|
+
continue;
|
|
1671
|
+
if (candidate.querySelector('input, select, textarea, button'))
|
|
1672
|
+
continue;
|
|
1673
|
+
const text = candidate.textContent?.trim();
|
|
1674
|
+
if (!text)
|
|
1675
|
+
continue;
|
|
1676
|
+
return text;
|
|
1677
|
+
}
|
|
1678
|
+
cur = cur.parentElement;
|
|
1679
|
+
depth++;
|
|
1680
|
+
}
|
|
1681
|
+
return null;
|
|
1682
|
+
}
|
|
1683
|
+
// Try the presentational read first — most accurate for react-select /
|
|
1684
|
+
// Radix style libraries.
|
|
1685
|
+
const presentational = readPresentational(el);
|
|
1686
|
+
if (presentational)
|
|
1687
|
+
return presentational;
|
|
1688
|
+
// Fall back to the trigger's own textContent. For libraries that don't
|
|
1689
|
+
// use a sibling presentational element (Lever, plain ARIA listboxes,
|
|
1690
|
+
// some Ashby forms), the trigger itself shows the current selection or
|
|
1691
|
+
// the placeholder text directly.
|
|
1692
|
+
const direct = el.textContent?.trim();
|
|
1693
|
+
return direct || null;
|
|
1694
|
+
});
|
|
1695
|
+
if (!visibleText)
|
|
1696
|
+
return false;
|
|
1697
|
+
// PLACEHOLDER_PATTERN already exists in this module — reuse it so the
|
|
1698
|
+
// detection rules stay in one place.
|
|
1699
|
+
return PLACEHOLDER_PATTERN.test(visibleText);
|
|
1700
|
+
}
|
|
1701
|
+
catch {
|
|
1702
|
+
return false;
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1556
1705
|
/**
|
|
1557
1706
|
* Read the picked option from a custom combobox's sibling presentational
|
|
1558
1707
|
* element ONLY. Skips the input's own .value (which is the typed search
|
|
@@ -1764,6 +1913,14 @@ async function confirmListboxSelection(page, fieldLabel, label, exact, anchor, c
|
|
|
1764
1913
|
const popupState = await collectVisibleOptionHints(page, anchor);
|
|
1765
1914
|
return !popupState.hasPopup;
|
|
1766
1915
|
};
|
|
1916
|
+
// ARIA veto: if the combobox still advertises aria-invalid="true" after the
|
|
1917
|
+
// option click, the library itself is telling us the commit did not land.
|
|
1918
|
+
// Displayed-value reads can still return stale placeholder text or an in-
|
|
1919
|
+
// flight sibling value during react-select's closing animation, so trusting
|
|
1920
|
+
// them without consulting aria-invalid produces the silent-success bug this
|
|
1921
|
+
// whole function exists to prevent. See readAriaInvalid() for why this is
|
|
1922
|
+
// the right generic signal.
|
|
1923
|
+
const ariaVeto = async () => currentHandle ? readAriaInvalid(currentHandle) : false;
|
|
1767
1924
|
if (currentHandle) {
|
|
1768
1925
|
// Sibling-only check first: react-select / Radix / similar libraries
|
|
1769
1926
|
// only populate `.select__single-value` (and friends) AFTER they commit
|
|
@@ -1772,11 +1929,13 @@ async function confirmListboxSelection(page, fieldLabel, label, exact, anchor, c
|
|
|
1772
1929
|
// fast-path for the v1.34.0 silent-fill bug.
|
|
1773
1930
|
const trustedSibling = await trustedSiblingComboboxValue(currentHandle);
|
|
1774
1931
|
if (trustedSibling && displayedValueMatchesSelection(trustedSibling, label, exact, selectedOptionText)) {
|
|
1775
|
-
|
|
1932
|
+
if (!(await ariaVeto()))
|
|
1933
|
+
return true;
|
|
1776
1934
|
}
|
|
1777
1935
|
const immediateValues = await elementHandleDisplayedValues(currentHandle);
|
|
1778
1936
|
if (immediateValues.some(value => displayedValueMatchesSelection(value, label, exact, selectedOptionText)) &&
|
|
1779
|
-
await canTrustEditableDisplayMatch()
|
|
1937
|
+
await canTrustEditableDisplayMatch() &&
|
|
1938
|
+
!(await ariaVeto())) {
|
|
1780
1939
|
return true;
|
|
1781
1940
|
}
|
|
1782
1941
|
}
|
|
@@ -1788,7 +1947,8 @@ async function confirmListboxSelection(page, fieldLabel, label, exact, anchor, c
|
|
|
1788
1947
|
// schedule state updates via React transitions.
|
|
1789
1948
|
const trustedSibling = await trustedSiblingComboboxValue(currentHandle);
|
|
1790
1949
|
if (trustedSibling && displayedValueMatchesSelection(trustedSibling, label, exact, selectedOptionText)) {
|
|
1791
|
-
|
|
1950
|
+
if (!(await ariaVeto()))
|
|
1951
|
+
return true;
|
|
1792
1952
|
}
|
|
1793
1953
|
}
|
|
1794
1954
|
for (const frame of page.frames()) {
|
|
@@ -1797,11 +1957,12 @@ async function confirmListboxSelection(page, fieldLabel, label, exact, anchor, c
|
|
|
1797
1957
|
continue;
|
|
1798
1958
|
const values = await locatorDisplayedValues(locator);
|
|
1799
1959
|
if (values.some(value => displayedValueMatchesSelection(value, label, exact, selectedOptionText)) &&
|
|
1800
|
-
await canTrustEditableDisplayMatch()
|
|
1960
|
+
await canTrustEditableDisplayMatch() &&
|
|
1961
|
+
!(await ariaVeto())) {
|
|
1801
1962
|
return true;
|
|
1802
1963
|
}
|
|
1803
1964
|
}
|
|
1804
|
-
if (await visibleOptionIsSelected(page, label, exact, anchor))
|
|
1965
|
+
if (await visibleOptionIsSelected(page, label, exact, anchor) && !(await ariaVeto()))
|
|
1805
1966
|
return true;
|
|
1806
1967
|
// Check for multi-select chips/tags (Greenhouse-style React-Select multi)
|
|
1807
1968
|
if (currentHandle) {
|
|
@@ -1819,7 +1980,7 @@ async function confirmListboxSelection(page, fieldLabel, label, exact, anchor, c
|
|
|
1819
1980
|
}
|
|
1820
1981
|
return false;
|
|
1821
1982
|
}, label);
|
|
1822
|
-
if (hasChip)
|
|
1983
|
+
if (hasChip && !(await ariaVeto()))
|
|
1823
1984
|
return true;
|
|
1824
1985
|
}
|
|
1825
1986
|
catch { /* handle detached */ }
|
|
@@ -1898,11 +2059,34 @@ async function dismissAndReVerifySelection(page, label, exact, currentHandle, se
|
|
|
1898
2059
|
}
|
|
1899
2060
|
await delay(100);
|
|
1900
2061
|
}
|
|
1901
|
-
// Deadline elapsed without a positive signal.
|
|
1902
|
-
//
|
|
1903
|
-
//
|
|
1904
|
-
//
|
|
1905
|
-
|
|
2062
|
+
// Deadline elapsed without a positive signal. Two scenarios:
|
|
2063
|
+
//
|
|
2064
|
+
// (a) sawAnyValue === true:
|
|
2065
|
+
// The field did report *some* displayed value, but none of them
|
|
2066
|
+
// matched the target label. That's a silent revert — React Select
|
|
2067
|
+
// restored the placeholder after the blur event. Return false so the
|
|
2068
|
+
// caller retries via keyboard or surfaces the failure.
|
|
2069
|
+
//
|
|
2070
|
+
// (b) sawAnyValue === false:
|
|
2071
|
+
// The field never reported a displayed value during the poll. Used
|
|
2072
|
+
// to return true here (legacy "optimistic success so well-formed
|
|
2073
|
+
// selects don't suddenly fail"), but that's exactly how the silent
|
|
2074
|
+
// commit bug reached production: an unfilled required combobox with
|
|
2075
|
+
// only a sr-only placeholder would pass through this branch despite
|
|
2076
|
+
// still being empty. The authoritative checks are aria-invalid AND
|
|
2077
|
+
// the trigger's visible text — if the library still marks the trigger
|
|
2078
|
+
// invalid, OR the trigger still shows a "Select..." placeholder, it
|
|
2079
|
+
// is NOT committed, full stop. Only if BOTH signals are clear do we
|
|
2080
|
+
// keep the legacy optimism so well-formed controls that never expose
|
|
2081
|
+
// a displayed value (pure ARIA patterns like Radix with SelectValue
|
|
2082
|
+
// empty text) still pass.
|
|
2083
|
+
if (sawAnyValue)
|
|
2084
|
+
return false;
|
|
2085
|
+
if (await readAriaInvalid(currentHandle))
|
|
2086
|
+
return false;
|
|
2087
|
+
if (await readTriggerShowsPlaceholder(currentHandle))
|
|
2088
|
+
return false;
|
|
2089
|
+
return true;
|
|
1906
2090
|
}
|
|
1907
2091
|
/**
|
|
1908
2092
|
* Resolve and validate paths on the machine running the proxy (not the agent host).
|
|
@@ -1926,14 +2110,30 @@ export function resolveExistingFiles(rawPaths) {
|
|
|
1926
2110
|
return paths;
|
|
1927
2111
|
}
|
|
1928
2112
|
async function findLabeledFileInput(frame, fieldLabel, exact) {
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
2113
|
+
// Try exact-match candidates first, even when the caller passed
|
|
2114
|
+
// exact=false. Same Greenhouse-style failure mode as findLabeledControl:
|
|
2115
|
+
// a substring match (e.g. file labeled "Resume" hijacked by another
|
|
2116
|
+
// "Please attach your resume below" field) silently picks the wrong
|
|
2117
|
+
// input. Trying exact first guarantees the literal label wins when
|
|
2118
|
+
// present, and only falls through to substring matching otherwise.
|
|
2119
|
+
const exactDirect = frame.getByLabel(fieldLabel, { exact: true });
|
|
2120
|
+
const exactDirectCount = await exactDirect.count();
|
|
2121
|
+
for (let i = 0; i < exactDirectCount; i++) {
|
|
2122
|
+
const candidate = exactDirect.nth(i);
|
|
1933
2123
|
const isFileInput = await candidate.evaluate(el => el instanceof HTMLInputElement && el.type === 'file').catch(() => false);
|
|
1934
2124
|
if (isFileInput)
|
|
1935
2125
|
return candidate;
|
|
1936
2126
|
}
|
|
2127
|
+
if (!exact) {
|
|
2128
|
+
const direct = frame.getByLabel(fieldLabel, { exact: false });
|
|
2129
|
+
const directCount = await direct.count();
|
|
2130
|
+
for (let i = 0; i < directCount; i++) {
|
|
2131
|
+
const candidate = direct.nth(i);
|
|
2132
|
+
const isFileInput = await candidate.evaluate(el => el instanceof HTMLInputElement && el.type === 'file').catch(() => false);
|
|
2133
|
+
if (isFileInput)
|
|
2134
|
+
return candidate;
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
1937
2137
|
const loc = frame.locator('input[type="file"]');
|
|
1938
2138
|
const count = await loc.count();
|
|
1939
2139
|
if (count === 0)
|
|
@@ -2201,13 +2401,20 @@ async function findLabeledEditableField(page, fieldLabel, exact, cache, fieldId)
|
|
|
2201
2401
|
if (cached !== undefined)
|
|
2202
2402
|
return cached;
|
|
2203
2403
|
for (const frame of page.frames()) {
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2404
|
+
// Always try exact-match candidates first, even when the caller passed
|
|
2405
|
+
// exact=false. Same Greenhouse-style failure as findLabeledControl: a
|
|
2406
|
+
// text field labeled "Authorization code" gets silently hijacked by
|
|
2407
|
+
// a sibling react-select labeled "Are you legally authorized to work
|
|
2408
|
+
// in the country in which you are applying?" because the substring
|
|
2409
|
+
// pass matches "auth" in both. Trying exact first guarantees the
|
|
2410
|
+
// literal label wins when present.
|
|
2411
|
+
const exactCandidates = [
|
|
2412
|
+
frame.getByLabel(fieldLabel, { exact: true }),
|
|
2413
|
+
frame.getByPlaceholder(fieldLabel, { exact: true }),
|
|
2414
|
+
frame.getByRole('textbox', { name: fieldLabel, exact: true }),
|
|
2415
|
+
frame.getByRole('combobox', { name: fieldLabel, exact: true }),
|
|
2209
2416
|
];
|
|
2210
|
-
for (const candidate of
|
|
2417
|
+
for (const candidate of exactCandidates) {
|
|
2211
2418
|
const visible = await firstVisible(candidate, { minWidth: 1, minHeight: 1 });
|
|
2212
2419
|
if (!visible)
|
|
2213
2420
|
continue;
|
|
@@ -2216,6 +2423,23 @@ async function findLabeledEditableField(page, fieldLabel, exact, cache, fieldId)
|
|
|
2216
2423
|
return visible;
|
|
2217
2424
|
}
|
|
2218
2425
|
}
|
|
2426
|
+
if (!exact) {
|
|
2427
|
+
const candidates = [
|
|
2428
|
+
frame.getByLabel(fieldLabel, { exact: false }),
|
|
2429
|
+
frame.getByPlaceholder(fieldLabel, { exact: false }),
|
|
2430
|
+
frame.getByRole('textbox', { name: fieldLabel, exact: false }),
|
|
2431
|
+
frame.getByRole('combobox', { name: fieldLabel, exact: false }),
|
|
2432
|
+
];
|
|
2433
|
+
for (const candidate of candidates) {
|
|
2434
|
+
const visible = await firstVisible(candidate, { minWidth: 1, minHeight: 1 });
|
|
2435
|
+
if (!visible)
|
|
2436
|
+
continue;
|
|
2437
|
+
if (await locatorIsEditable(visible)) {
|
|
2438
|
+
writeCachedLocator(cache, 'editable', fieldLabel, exact, fieldId, visible);
|
|
2439
|
+
return visible;
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2219
2443
|
const fallback = await findLabeledControl(frame, fieldLabel, exact);
|
|
2220
2444
|
if (fallback && await locatorIsEditable(fallback)) {
|
|
2221
2445
|
writeCachedLocator(cache, 'editable', fieldLabel, exact, fieldId, fallback);
|
|
@@ -3166,9 +3390,50 @@ export async function pickListboxOption(page, label, opts) {
|
|
|
3166
3390
|
}
|
|
3167
3391
|
return false;
|
|
3168
3392
|
};
|
|
3393
|
+
/**
|
|
3394
|
+
* Final post-commit sanity check. Even after `confirmListboxSelection` and
|
|
3395
|
+
* `dismissAndReVerifySelection` say the option was committed, two distinct
|
|
3396
|
+
* silent-failure modes can leave the field empty:
|
|
3397
|
+
*
|
|
3398
|
+
* 1. The library reverts the selection a few frames later by flipping
|
|
3399
|
+
* `aria-invalid` back to "true" (Workday PTX, certain react-select
|
|
3400
|
+
* forks, some Ashby flows). The aria-invalid attribute is the
|
|
3401
|
+
* authoritative signal here.
|
|
3402
|
+
*
|
|
3403
|
+
* 2. The library never commits the selection but ALSO never sets
|
|
3404
|
+
* aria-invalid (Greenhouse, Lever, plain ARIA listboxes). The trigger
|
|
3405
|
+
* stays at the "Select..." placeholder until the user submits the
|
|
3406
|
+
* form, at which point validation finally fires. There is no aria
|
|
3407
|
+
* flag during the silent window, so the only reliable signal is the
|
|
3408
|
+
* trigger's visible text — if it still matches the placeholder
|
|
3409
|
+
* pattern, the field is empty regardless of what the verification
|
|
3410
|
+
* heuristics said.
|
|
3411
|
+
*
|
|
3412
|
+
* Both checks run together because they cover disjoint library patterns
|
|
3413
|
+
* and a failed commit on either signal is a failed commit. Returns true
|
|
3414
|
+
* only when BOTH signals say the field looks committed.
|
|
3415
|
+
*
|
|
3416
|
+
* This is defense-in-depth: confirmListboxSelection already vetos on
|
|
3417
|
+
* aria-invalid, but that check runs while the popup is still closing.
|
|
3418
|
+
* This check runs after the popup is fully gone, giving the library a
|
|
3419
|
+
* chance to expose its final committed state.
|
|
3420
|
+
*/
|
|
3421
|
+
const postCommitVerify = async () => {
|
|
3422
|
+
if (!openedHandle)
|
|
3423
|
+
return true;
|
|
3424
|
+
// Give React Select a couple of animation frames to settle its final
|
|
3425
|
+
// aria-invalid state after the blur/commit. The cost is a few ms on the
|
|
3426
|
+
// happy path and a correct failure signal on the unhappy one.
|
|
3427
|
+
await delay(40);
|
|
3428
|
+
if (await readAriaInvalid(openedHandle))
|
|
3429
|
+
return false;
|
|
3430
|
+
if (await readTriggerShowsPlaceholder(openedHandle))
|
|
3431
|
+
return false;
|
|
3432
|
+
return true;
|
|
3433
|
+
};
|
|
3169
3434
|
try {
|
|
3170
3435
|
if (await attemptClickSelection()) {
|
|
3171
|
-
if (await dismissAfterSelection())
|
|
3436
|
+
if (await dismissAfterSelection() && await postCommitVerify())
|
|
3172
3437
|
return;
|
|
3173
3438
|
}
|
|
3174
3439
|
let visibleHints = await collectVisibleOptionHints(page, anchor);
|
|
@@ -3181,7 +3446,7 @@ export async function pickListboxOption(page, label, opts) {
|
|
|
3181
3446
|
// next attempt still searches inside the right container.
|
|
3182
3447
|
await refreshPopupScope();
|
|
3183
3448
|
if (await attemptClickSelection()) {
|
|
3184
|
-
if (await dismissAfterSelection())
|
|
3449
|
+
if (await dismissAfterSelection() && await postCommitVerify())
|
|
3185
3450
|
return;
|
|
3186
3451
|
}
|
|
3187
3452
|
visibleHints = await collectVisibleOptionHints(page, anchor);
|
|
@@ -3195,7 +3460,7 @@ export async function pickListboxOption(page, label, opts) {
|
|
|
3195
3460
|
await confirmListboxSelection(page, opts.fieldLabel, label, exact, anchor, openedHandle, selectedOptionText, {
|
|
3196
3461
|
editable: openedEditable,
|
|
3197
3462
|
})) {
|
|
3198
|
-
if (await dismissAfterSelection())
|
|
3463
|
+
if (await dismissAfterSelection() && await postCommitVerify())
|
|
3199
3464
|
return;
|
|
3200
3465
|
}
|
|
3201
3466
|
}
|