@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.
@@ -1 +1 @@
1
- {"version":3,"file":"dom-actions.d.ts","sourceRoot":"","sources":["../src/dom-actions.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"dom-actions.d.ts","sourceRoot":"","sources":["../src/dom-actions.ts"],"names":[],"mappings":"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"}
@@ -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
- return true;
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
- return true;
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. If the field never reported any displayed
1902
- // value, the previous confirmListboxSelection step is the only signal we have — keep the
1903
- // legacy optimistic behavior so well-formed selects don't suddenly fail.
1904
- // If the field DID report values but none of them matched, treat that as a silent revert.
1905
- return !sawAnyValue;
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
- const direct = frame.getByLabel(fieldLabel, { exact });
1930
- const directCount = await direct.count();
1931
- for (let i = 0; i < directCount; i++) {
1932
- const candidate = direct.nth(i);
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
- const candidates = [
2205
- frame.getByLabel(fieldLabel, { exact }),
2206
- frame.getByPlaceholder(fieldLabel, { exact }),
2207
- frame.getByRole('textbox', { name: fieldLabel, exact }),
2208
- frame.getByRole('combobox', { name: fieldLabel, exact }),
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 candidates) {
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
  }