@geometra/proxy 1.36.0 → 1.38.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,55 @@ 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
+ }
1556
1605
  /**
1557
1606
  * Read the picked option from a custom combobox's sibling presentational
1558
1607
  * element ONLY. Skips the input's own .value (which is the typed search
@@ -1764,6 +1813,14 @@ async function confirmListboxSelection(page, fieldLabel, label, exact, anchor, c
1764
1813
  const popupState = await collectVisibleOptionHints(page, anchor);
1765
1814
  return !popupState.hasPopup;
1766
1815
  };
1816
+ // ARIA veto: if the combobox still advertises aria-invalid="true" after the
1817
+ // option click, the library itself is telling us the commit did not land.
1818
+ // Displayed-value reads can still return stale placeholder text or an in-
1819
+ // flight sibling value during react-select's closing animation, so trusting
1820
+ // them without consulting aria-invalid produces the silent-success bug this
1821
+ // whole function exists to prevent. See readAriaInvalid() for why this is
1822
+ // the right generic signal.
1823
+ const ariaVeto = async () => currentHandle ? readAriaInvalid(currentHandle) : false;
1767
1824
  if (currentHandle) {
1768
1825
  // Sibling-only check first: react-select / Radix / similar libraries
1769
1826
  // only populate `.select__single-value` (and friends) AFTER they commit
@@ -1772,11 +1829,13 @@ async function confirmListboxSelection(page, fieldLabel, label, exact, anchor, c
1772
1829
  // fast-path for the v1.34.0 silent-fill bug.
1773
1830
  const trustedSibling = await trustedSiblingComboboxValue(currentHandle);
1774
1831
  if (trustedSibling && displayedValueMatchesSelection(trustedSibling, label, exact, selectedOptionText)) {
1775
- return true;
1832
+ if (!(await ariaVeto()))
1833
+ return true;
1776
1834
  }
1777
1835
  const immediateValues = await elementHandleDisplayedValues(currentHandle);
1778
1836
  if (immediateValues.some(value => displayedValueMatchesSelection(value, label, exact, selectedOptionText)) &&
1779
- await canTrustEditableDisplayMatch()) {
1837
+ await canTrustEditableDisplayMatch() &&
1838
+ !(await ariaVeto())) {
1780
1839
  return true;
1781
1840
  }
1782
1841
  }
@@ -1788,7 +1847,8 @@ async function confirmListboxSelection(page, fieldLabel, label, exact, anchor, c
1788
1847
  // schedule state updates via React transitions.
1789
1848
  const trustedSibling = await trustedSiblingComboboxValue(currentHandle);
1790
1849
  if (trustedSibling && displayedValueMatchesSelection(trustedSibling, label, exact, selectedOptionText)) {
1791
- return true;
1850
+ if (!(await ariaVeto()))
1851
+ return true;
1792
1852
  }
1793
1853
  }
1794
1854
  for (const frame of page.frames()) {
@@ -1797,11 +1857,12 @@ async function confirmListboxSelection(page, fieldLabel, label, exact, anchor, c
1797
1857
  continue;
1798
1858
  const values = await locatorDisplayedValues(locator);
1799
1859
  if (values.some(value => displayedValueMatchesSelection(value, label, exact, selectedOptionText)) &&
1800
- await canTrustEditableDisplayMatch()) {
1860
+ await canTrustEditableDisplayMatch() &&
1861
+ !(await ariaVeto())) {
1801
1862
  return true;
1802
1863
  }
1803
1864
  }
1804
- if (await visibleOptionIsSelected(page, label, exact, anchor))
1865
+ if (await visibleOptionIsSelected(page, label, exact, anchor) && !(await ariaVeto()))
1805
1866
  return true;
1806
1867
  // Check for multi-select chips/tags (Greenhouse-style React-Select multi)
1807
1868
  if (currentHandle) {
@@ -1819,7 +1880,7 @@ async function confirmListboxSelection(page, fieldLabel, label, exact, anchor, c
1819
1880
  }
1820
1881
  return false;
1821
1882
  }, label);
1822
- if (hasChip)
1883
+ if (hasChip && !(await ariaVeto()))
1823
1884
  return true;
1824
1885
  }
1825
1886
  catch { /* handle detached */ }
@@ -1898,11 +1959,30 @@ async function dismissAndReVerifySelection(page, label, exact, currentHandle, se
1898
1959
  }
1899
1960
  await delay(100);
1900
1961
  }
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;
1962
+ // Deadline elapsed without a positive signal. Two scenarios:
1963
+ //
1964
+ // (a) sawAnyValue === true:
1965
+ // The field did report *some* displayed value, but none of them
1966
+ // matched the target label. That's a silent revert — React Select
1967
+ // restored the placeholder after the blur event. Return false so the
1968
+ // caller retries via keyboard or surfaces the failure.
1969
+ //
1970
+ // (b) sawAnyValue === false:
1971
+ // The field never reported a displayed value during the poll. Used
1972
+ // to return true here (legacy "optimistic success so well-formed
1973
+ // selects don't suddenly fail"), but that's exactly how the silent
1974
+ // commit bug reached production: an unfilled required combobox with
1975
+ // only a sr-only placeholder would pass through this branch despite
1976
+ // still being empty. The authoritative check is aria-invalid: if the
1977
+ // library still marks the trigger invalid, it is NOT committed, full
1978
+ // stop. If aria-invalid is absent or false, keep the legacy optimism
1979
+ // so well-formed controls that never expose a displayed value (pure
1980
+ // ARIA patterns like Radix with SelectValue empty text) still pass.
1981
+ if (sawAnyValue)
1982
+ return false;
1983
+ if (await readAriaInvalid(currentHandle))
1984
+ return false;
1985
+ return true;
1906
1986
  }
1907
1987
  /**
1908
1988
  * Resolve and validate paths on the machine running the proxy (not the agent host).
@@ -1926,14 +2006,30 @@ export function resolveExistingFiles(rawPaths) {
1926
2006
  return paths;
1927
2007
  }
1928
2008
  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);
2009
+ // Try exact-match candidates first, even when the caller passed
2010
+ // exact=false. Same Greenhouse-style failure mode as findLabeledControl:
2011
+ // a substring match (e.g. file labeled "Resume" hijacked by another
2012
+ // "Please attach your resume below" field) silently picks the wrong
2013
+ // input. Trying exact first guarantees the literal label wins when
2014
+ // present, and only falls through to substring matching otherwise.
2015
+ const exactDirect = frame.getByLabel(fieldLabel, { exact: true });
2016
+ const exactDirectCount = await exactDirect.count();
2017
+ for (let i = 0; i < exactDirectCount; i++) {
2018
+ const candidate = exactDirect.nth(i);
1933
2019
  const isFileInput = await candidate.evaluate(el => el instanceof HTMLInputElement && el.type === 'file').catch(() => false);
1934
2020
  if (isFileInput)
1935
2021
  return candidate;
1936
2022
  }
2023
+ if (!exact) {
2024
+ const direct = frame.getByLabel(fieldLabel, { exact: false });
2025
+ const directCount = await direct.count();
2026
+ for (let i = 0; i < directCount; i++) {
2027
+ const candidate = direct.nth(i);
2028
+ const isFileInput = await candidate.evaluate(el => el instanceof HTMLInputElement && el.type === 'file').catch(() => false);
2029
+ if (isFileInput)
2030
+ return candidate;
2031
+ }
2032
+ }
1937
2033
  const loc = frame.locator('input[type="file"]');
1938
2034
  const count = await loc.count();
1939
2035
  if (count === 0)
@@ -2201,13 +2297,20 @@ async function findLabeledEditableField(page, fieldLabel, exact, cache, fieldId)
2201
2297
  if (cached !== undefined)
2202
2298
  return cached;
2203
2299
  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 }),
2300
+ // Always try exact-match candidates first, even when the caller passed
2301
+ // exact=false. Same Greenhouse-style failure as findLabeledControl: a
2302
+ // text field labeled "Authorization code" gets silently hijacked by
2303
+ // a sibling react-select labeled "Are you legally authorized to work
2304
+ // in the country in which you are applying?" because the substring
2305
+ // pass matches "auth" in both. Trying exact first guarantees the
2306
+ // literal label wins when present.
2307
+ const exactCandidates = [
2308
+ frame.getByLabel(fieldLabel, { exact: true }),
2309
+ frame.getByPlaceholder(fieldLabel, { exact: true }),
2310
+ frame.getByRole('textbox', { name: fieldLabel, exact: true }),
2311
+ frame.getByRole('combobox', { name: fieldLabel, exact: true }),
2209
2312
  ];
2210
- for (const candidate of candidates) {
2313
+ for (const candidate of exactCandidates) {
2211
2314
  const visible = await firstVisible(candidate, { minWidth: 1, minHeight: 1 });
2212
2315
  if (!visible)
2213
2316
  continue;
@@ -2216,6 +2319,23 @@ async function findLabeledEditableField(page, fieldLabel, exact, cache, fieldId)
2216
2319
  return visible;
2217
2320
  }
2218
2321
  }
2322
+ if (!exact) {
2323
+ const candidates = [
2324
+ frame.getByLabel(fieldLabel, { exact: false }),
2325
+ frame.getByPlaceholder(fieldLabel, { exact: false }),
2326
+ frame.getByRole('textbox', { name: fieldLabel, exact: false }),
2327
+ frame.getByRole('combobox', { name: fieldLabel, exact: false }),
2328
+ ];
2329
+ for (const candidate of candidates) {
2330
+ const visible = await firstVisible(candidate, { minWidth: 1, minHeight: 1 });
2331
+ if (!visible)
2332
+ continue;
2333
+ if (await locatorIsEditable(visible)) {
2334
+ writeCachedLocator(cache, 'editable', fieldLabel, exact, fieldId, visible);
2335
+ return visible;
2336
+ }
2337
+ }
2338
+ }
2219
2339
  const fallback = await findLabeledControl(frame, fieldLabel, exact);
2220
2340
  if (fallback && await locatorIsEditable(fallback)) {
2221
2341
  writeCachedLocator(cache, 'editable', fieldLabel, exact, fieldId, fallback);
@@ -3166,9 +3286,32 @@ export async function pickListboxOption(page, label, opts) {
3166
3286
  }
3167
3287
  return false;
3168
3288
  };
3289
+ /**
3290
+ * Final post-commit sanity check. Even after `confirmListboxSelection` and
3291
+ * `dismissAndReVerifySelection` say the option was committed, some libraries
3292
+ * (Greenhouse's forked react-select, Workday PTX, old Ashby forms) can
3293
+ * revert the selection a few frames later when the library re-runs its
3294
+ * validator on the next render. The aria-invalid attribute is the
3295
+ * authoritative signal — if the trigger advertises invalid after all the
3296
+ * verification steps, the commit did not persist, full stop.
3297
+ *
3298
+ * This is defense-in-depth: confirmListboxSelection already vetos on
3299
+ * aria-invalid, but that check runs while the popup is still closing.
3300
+ * This check runs after the popup is fully gone, giving the library a
3301
+ * chance to expose its final committed state.
3302
+ */
3303
+ const postCommitAriaValid = async () => {
3304
+ if (!openedHandle)
3305
+ return true;
3306
+ // Give React Select a couple of animation frames to settle its final
3307
+ // aria-invalid state after the blur/commit. The cost is a few ms on the
3308
+ // happy path and a correct failure signal on the unhappy one.
3309
+ await delay(40);
3310
+ return !(await readAriaInvalid(openedHandle));
3311
+ };
3169
3312
  try {
3170
3313
  if (await attemptClickSelection()) {
3171
- if (await dismissAfterSelection())
3314
+ if (await dismissAfterSelection() && await postCommitAriaValid())
3172
3315
  return;
3173
3316
  }
3174
3317
  let visibleHints = await collectVisibleOptionHints(page, anchor);
@@ -3181,7 +3324,7 @@ export async function pickListboxOption(page, label, opts) {
3181
3324
  // next attempt still searches inside the right container.
3182
3325
  await refreshPopupScope();
3183
3326
  if (await attemptClickSelection()) {
3184
- if (await dismissAfterSelection())
3327
+ if (await dismissAfterSelection() && await postCommitAriaValid())
3185
3328
  return;
3186
3329
  }
3187
3330
  visibleHints = await collectVisibleOptionHints(page, anchor);
@@ -3195,7 +3338,7 @@ export async function pickListboxOption(page, label, opts) {
3195
3338
  await confirmListboxSelection(page, opts.fieldLabel, label, exact, anchor, openedHandle, selectedOptionText, {
3196
3339
  editable: openedEditable,
3197
3340
  })) {
3198
- if (await dismissAfterSelection())
3341
+ if (await dismissAfterSelection() && await postCommitAriaValid())
3199
3342
  return;
3200
3343
  }
3201
3344
  }