@enricai/barnacle 1.12.37 → 1.12.39

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.
Files changed (52) hide show
  1. package/dist/api/schemas/submissions.d.ts +10 -0
  2. package/dist/api/schemas/submissions.d.ts.map +1 -1
  3. package/dist/config.d.ts +14 -0
  4. package/dist/config.d.ts.map +1 -1
  5. package/dist/config.js +3 -0
  6. package/dist/config.js.map +1 -1
  7. package/dist/lib/telemetry/reconciliation-record.d.ts +28 -0
  8. package/dist/lib/telemetry/reconciliation-record.d.ts.map +1 -1
  9. package/dist/lib/telemetry/reconciliation-record.js +19 -1
  10. package/dist/lib/telemetry/reconciliation-record.js.map +1 -1
  11. package/dist/lib/telemetry/run-telemetry.d.ts +15 -0
  12. package/dist/lib/telemetry/run-telemetry.d.ts.map +1 -1
  13. package/dist/lib/telemetry/run-telemetry.js +10 -0
  14. package/dist/lib/telemetry/run-telemetry.js.map +1 -1
  15. package/dist/lib/telemetry/submission-capture.d.ts +5 -0
  16. package/dist/lib/telemetry/submission-capture.d.ts.map +1 -1
  17. package/dist/plugins/loader.d.ts.map +1 -1
  18. package/dist/plugins/loader.js +35 -0
  19. package/dist/plugins/loader.js.map +1 -1
  20. package/dist/scraper/captcha-callback-capture.d.ts +16 -11
  21. package/dist/scraper/captcha-callback-capture.d.ts.map +1 -1
  22. package/dist/scraper/captcha-callback-capture.js +53 -9
  23. package/dist/scraper/captcha-callback-capture.js.map +1 -1
  24. package/dist/scraper/errors.d.ts +39 -6
  25. package/dist/scraper/errors.d.ts.map +1 -1
  26. package/dist/scraper/errors.js +51 -7
  27. package/dist/scraper/errors.js.map +1 -1
  28. package/dist/scraper/flow-runner.d.ts +82 -5
  29. package/dist/scraper/flow-runner.d.ts.map +1 -1
  30. package/dist/scraper/flow-runner.js +268 -20
  31. package/dist/scraper/flow-runner.js.map +1 -1
  32. package/dist/scraper/http-client.d.ts.map +1 -1
  33. package/dist/scraper/http-client.js +40 -3
  34. package/dist/scraper/http-client.js.map +1 -1
  35. package/dist/scraper/session-create-limiter.d.ts +8 -0
  36. package/dist/scraper/session-create-limiter.d.ts.map +1 -0
  37. package/dist/scraper/session-create-limiter.js +80 -0
  38. package/dist/scraper/session-create-limiter.js.map +1 -0
  39. package/dist/scraper/session.d.ts.map +1 -1
  40. package/dist/scraper/session.js +6 -3
  41. package/dist/scraper/session.js.map +1 -1
  42. package/dist/scripts/recon-generate-multicall-fixture.d.ts +16 -0
  43. package/dist/scripts/recon-generate-multicall-fixture.d.ts.map +1 -1
  44. package/dist/scripts/recon-generate-multicall-fixture.js +34 -0
  45. package/dist/scripts/recon-generate-multicall-fixture.js.map +1 -1
  46. package/dist/scripts/recon-generate.d.ts +21 -6
  47. package/dist/scripts/recon-generate.d.ts.map +1 -1
  48. package/dist/scripts/recon-generate.js +29 -8
  49. package/dist/scripts/recon-generate.js.map +1 -1
  50. package/dist/site-plugin.d.ts +10 -0
  51. package/dist/site-plugin.d.ts.map +1 -1
  52. package/package.json +1 -1
@@ -14,7 +14,7 @@
14
14
  * the terminal failure dump.
15
15
  */
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.TRAILING_GRACE_WINDOW = exports.STEP_WATCHDOG_MS = exports.GOTO_TIMEOUT_MS = void 0;
17
+ exports.TRAILING_GRACE_WINDOW = exports.STEP_WATCHDOG_MS = exports.OPENER_PAIRED_HIDDEN_SELECT_EL_EXPR = exports.GOTO_TIMEOUT_MS = exports.waitForSpaReady = void 0;
18
18
  exports.mergeResponseHeaders = mergeResponseHeaders;
19
19
  exports.wireSignalCapture = wireSignalCapture;
20
20
  exports.prepareFailureDumpBody = prepareFailureDumpBody;
@@ -75,7 +75,9 @@ exports.pollEnumerate = pollEnumerate;
75
75
  exports.waitForTransitionBody = waitForTransitionBody;
76
76
  exports.injectCaptchaTokenAndSubmit = injectCaptchaTokenAndSubmit;
77
77
  exports.submitCaptchaGatedForm = submitCaptchaGatedForm;
78
+ exports.trySelectPrimitive = trySelectPrimitive;
78
79
  exports.chooseRequiredSelectOption = chooseRequiredSelectOption;
80
+ exports.tryFillRequiredSelectsPrimitive = tryFillRequiredSelectsPrimitive;
79
81
  exports.buildRadioIdXPath = buildRadioIdXPath;
80
82
  exports.selectRadioGroupOption = selectRadioGroupOption;
81
83
  exports.hasUnfilledRequiredControlForStep = hasUnfilledRequiredControlForStep;
@@ -125,6 +127,8 @@ const submit_control_1 = require("../scraper/submit-control");
125
127
  const watchdog_1 = require("../scraper/watchdog");
126
128
  const recon_shared_1 = require("../scripts/recon-shared");
127
129
  const client_1 = require("../testmail/client");
130
+ var spa_readiness_2 = require("../scraper/spa-readiness");
131
+ Object.defineProperty(exports, "waitForSpaReady", { enumerable: true, get: function () { return spa_readiness_2.waitForSpaReady; } });
128
132
  const logger = (0, logging_1.getLogger)({ name: "scraper/flow-runner" });
129
133
  /** Cap on the rolling capture-filename window held in memory for failure dumps. */
130
134
  const RECENT_CAPTURES_WINDOW = 20;
@@ -459,6 +463,36 @@ const PROMPT_TRIGGER_SELECTORS = [
459
463
  "[data-automation-id='multiSelectContainer']",
460
464
  "[data-automation-id='promptSelectionLabel']",
461
465
  ].join(",");
466
+ /**
467
+ * In-page predicate source for `(el) => boolean`: true when `el` is a
468
+ * `<select>` that is a design-system combobox opener's paired-but-hidden
469
+ * shadow control (Base Web `bb-customSelect` and any other vendor matching
470
+ * {@link PROMPT_TRIGGER_SELECTORS}) — a `<select>` with no layout box
471
+ * (`offsetParent === null`, the same idiom `DOM_SNAPSHOT_EXPR` uses for
472
+ * `display:none`-driven hiding classes like `dropdown-hide`) whose nearby
473
+ * container also holds a {@link PROMPT_TRIGGER_SELECTORS} opener element
474
+ * other than itself. The bounded-ancestor-climb-then-subtree-search shape
475
+ * mirrors {@link NEARBY_SELECTION_CONTAINER_FN_SRC}, but searches for
476
+ * {@link PROMPT_TRIGGER_SELECTORS} instead of the selection-marker union —
477
+ * this ties "opener" to the exact same union `tryPromptSelectorPrimitive`
478
+ * recognizes, so a select is excluded precisely when the primitive that
479
+ * should own it would actually claim it. Wired into every `<select>` list
480
+ * `trySelectPrimitive` and `applySelectValue` build, so those primitives fall
481
+ * through and let `tryPromptSelectorPrimitive` claim the opener instead — and
482
+ * consumed by {@link commitPromptOption}'s hidden-select-value corroboration
483
+ * when the opener's own readback is inconclusive.
484
+ */
485
+ exports.OPENER_PAIRED_HIDDEN_SELECT_EL_EXPR = `(el) => {
486
+ if (!el || el.tagName !== "SELECT" || el.offsetParent !== null) return false;
487
+ const triggerSel = ${JSON.stringify(PROMPT_TRIGGER_SELECTORS)};
488
+ let node = el.parentElement;
489
+ for (let depth = 0; depth < ${browser_click_expr_1.MAX_SELECTION_ANCESTOR_DEPTH} && node; depth++) {
490
+ const openers = node.querySelectorAll ? Array.from(node.querySelectorAll(triggerSel)) : [];
491
+ if (openers.some((opener) => opener !== el)) return true;
492
+ node = node.parentElement;
493
+ }
494
+ return false;
495
+ }`;
462
496
  /**
463
497
  * Cross-vendor selector union for an OPTION rendered inside the opened popup —
464
498
  * standards first (`role=option`), then the widget-kit option markers. Sibling
@@ -3058,7 +3092,7 @@ const XPATH_OF_FN_SRC = `(node) => {
3058
3092
  * popover isn't mistaken for a selection.
3059
3093
  */
3060
3094
  function selectionFingerprintObjSrc(elVar, dsVar) {
3061
- return `{ kind: ${elVar}.getAttribute("kind") || "", cls: ${elVar}.getAttribute("class") || "", ariaPressed: ${elVar}.getAttribute("aria-pressed") || "", ariaChecked: ${elVar}.getAttribute("aria-checked") || "", ariaSelected: ${elVar}.getAttribute("aria-selected") || "", dataState: (${dsVar} === "open" || ${dsVar} === "closed") ? "" : ${dsVar}, dataSelected: ${elVar}.hasAttribute("data-selected") ? "1" : "", dataChecked: ${elVar}.hasAttribute("data-checked") ? "1" : "", checked: (${elVar}.type === "checkbox" || ${elVar}.type === "radio") ? (${elVar}.checked ? "1" : "0") : "", value: typeof ${elVar}.value === "string" ? ${elVar}.value.slice(0, 200) : "" }`;
3095
+ return `{ kind: ${elVar}.getAttribute("kind") || "", cls: ${elVar}.getAttribute("class") || "", ariaPressed: ${elVar}.getAttribute("aria-pressed") || "", ariaChecked: ${elVar}.getAttribute("aria-checked") || "", ariaSelected: ${elVar}.getAttribute("aria-selected") || "", dataState: (${dsVar} === "open" || ${dsVar} === "closed") ? "" : ${dsVar}, dataSelected: ${elVar}.hasAttribute("data-selected") ? "1" : "", dataChecked: ${elVar}.hasAttribute("data-checked") ? "1" : "", checked: (${elVar}.type === "checkbox" || ${elVar}.type === "radio") ? (${elVar}.checked ? "1" : "0") : "", value: typeof ${elVar}.value === "string" ? ${elVar}.value.slice(0, 200) : "", ariaExpanded: ${elVar}.getAttribute("aria-expanded") || "" }`;
3062
3096
  }
3063
3097
  /**
3064
3098
  * Browser-side expression body that resolves `xpath` and returns that one
@@ -3143,7 +3177,7 @@ const SELECTION_STATE_MAP_EXPR = `(() => {
3143
3177
  const style = getComputedStyle(el);
3144
3178
  return style.display !== "none" && style.visibility !== "hidden";
3145
3179
  };
3146
- const sel = "button,[role=button],a,li,[tabindex],input,select,textarea,[role=option],[role=tab],[role=switch],[role=checkbox],[role=menuitemcheckbox]," + ${JSON.stringify(browser_click_expr_1.SELECTION_MARKER_CLASS_SELECTOR_SRC)};
3180
+ const sel = "button,[role=button],a,li,[tabindex],input,select,textarea,[role=option],[role=tab],[role=switch],[role=checkbox],[role=menuitemcheckbox],[role=combobox]," + ${JSON.stringify(browser_click_expr_1.SELECTION_MARKER_CLASS_SELECTOR_SRC)};
3147
3181
  const skip = (el) => el.closest("[role=dialog],[role=tooltip],[aria-live]") !== null;
3148
3182
  const isCommittedValueControl = (el) =>
3149
3183
  (el.tagName === "INPUT" || el.tagName === "SELECT") &&
@@ -3208,6 +3242,7 @@ function asSelectionFingerprint(raw) {
3208
3242
  dataChecked: typeof r.dataChecked === "string" ? r.dataChecked : "",
3209
3243
  checked: typeof r.checked === "string" ? r.checked : "",
3210
3244
  value: typeof r.value === "string" ? r.value : "",
3245
+ ariaExpanded: typeof r.ariaExpanded === "string" ? r.ariaExpanded : "",
3211
3246
  };
3212
3247
  }
3213
3248
  /**
@@ -3404,6 +3439,76 @@ async function selectionSiblingCommittedValueChanged(target, leafXpath, preSelec
3404
3439
  return false;
3405
3440
  }
3406
3441
  }
3442
+ /**
3443
+ * Dedicated pre/post check crediting exactly one case none of the fingerprint
3444
+ * diffs above can see: a raw click resolved onto (or under) a
3445
+ * {@link PROMPT_TRIGGER_SELECTORS}-shaped combobox opener that GENUINELY opened
3446
+ * its owned option panel. Opening a widget only flips `aria-expanded`, which
3447
+ * {@link selectionFingerprintObjSrc}'s shared fingerprint deliberately blanks out
3448
+ * of `dataState` and which {@link selectionFingerprintChanged} /
3449
+ * {@link selectionAncestorChanged}'s `changed()` never compare — by design, so a
3450
+ * bare accordion/tooltip disclosure is never mistaken for a selection. That
3451
+ * leaves a replanner-generated two-step "click to open the dropdown"
3452
+ * instruction with no signal at all: the fingerprint tiers above never move,
3453
+ * so the open-click is scored phantom even though it genuinely exposed the
3454
+ * option panel.
3455
+ *
3456
+ * This function is independent of those tiers rather than folded into their
3457
+ * `changed()`: it reads the new {@link ElementSelectionFingerprint.ariaExpanded}
3458
+ * field, but ONLY on a node that itself matches {@link PROMPT_TRIGGER_SELECTORS},
3459
+ * and ONLY credits it when the opener's `aria-owns`/`aria-controls` scope (the
3460
+ * SAME {@link PROMPT_SCOPE_ROOT_EXPR} resolution `tryPromptSelectorPrimitive`
3461
+ * uses) now actually contains {@link PROMPT_OPTION_SELECTORS} content — never a
3462
+ * bare `aria-expanded` flip with nothing rendered underneath. That second
3463
+ * condition is what keeps a bare accordion/tooltip trigger (which flips
3464
+ * `aria-expanded` but owns no option panel) excluded, preserving the existing
3465
+ * deliberate exclusion.
3466
+ *
3467
+ * Walks from the leaf up to {@link MAX_SELECTION_ANCESTOR_DEPTH} for the NEAREST
3468
+ * trigger match (mirrors {@link selectionAncestorChanged}'s climb, since
3469
+ * Stagehand may resolve the click onto a label/icon child of the opener rather
3470
+ * than the opener itself). No baseline entry for that trigger, no trigger
3471
+ * match, a trigger that was already expanded pre-click, or a trigger that's
3472
+ * still collapsed post-click → `false` (defer to the network/URL signal).
3473
+ * Returns `false` on any evaluate throw.
3474
+ */
3475
+ async function comboboxOpenerPanelOpened(target, leafXpath, preSelectionState) {
3476
+ const expr = `(() => {
3477
+ const LEAF = ${JSON.stringify(leafXpath)};
3478
+ const BASE = ${JSON.stringify(preSelectionState)};
3479
+ const TRIGGER_SEL = ${JSON.stringify(PROMPT_TRIGGER_SELECTORS)};
3480
+ const OPTION_SEL = ${JSON.stringify(PROMPT_OPTION_SELECTORS)};
3481
+ const xpathOf = ${XPATH_OF_FN_SRC};
3482
+ const scopeRootOf = ${PROMPT_SCOPE_ROOT_EXPR};
3483
+ const r = document.evaluate(LEAF, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
3484
+ let node = r.singleNodeValue;
3485
+ for (let depth = 0; depth < ${browser_click_expr_1.MAX_SELECTION_ANCESTOR_DEPTH} && node; depth++) {
3486
+ if (node.getAttribute && node.matches && node.matches(TRIGGER_SEL)) {
3487
+ const pre = BASE[xpathOf(node)];
3488
+ if (!pre) return false;
3489
+ const wasCollapsed = pre.ariaExpanded !== "true";
3490
+ const nowExpanded = (node.getAttribute("aria-expanded") || "") === "true";
3491
+ if (!wasCollapsed || !nowExpanded) return false;
3492
+ // scopeRootOf falls back to \`document\` when the trigger has no
3493
+ // aria-controls/aria-owns ref AND no inline popup in its own subtree.
3494
+ // That fallback is a page-wide search, not the opener's OWNED scope —
3495
+ // crediting through it would let an unrelated widget's already-open
3496
+ // options (elsewhere on the page) falsely credit THIS trigger's click.
3497
+ const scope = scopeRootOf(node);
3498
+ if (!scope || scope === document || !scope.querySelector) return false;
3499
+ return !!scope.querySelector(OPTION_SEL);
3500
+ }
3501
+ node = node.parentElement;
3502
+ }
3503
+ return false;
3504
+ })()`;
3505
+ try {
3506
+ return (await target.evaluate(expr)) === true;
3507
+ }
3508
+ catch {
3509
+ return false;
3510
+ }
3511
+ }
3407
3512
  /** How long the upload primitive waits for a post-setInputFiles network POST. */
3408
3513
  const UPLOAD_NETWORK_TIMEOUT_MS = 5_000;
3409
3514
  /** Polling interval while waiting for the upload's network signal. */
@@ -4312,10 +4417,14 @@ function findCaptchaCallbackExprSrc() {
4312
4417
  * `hcaptcha.execute(widgetId)`, letting the callback fire as a natural
4313
4418
  * consequence of hCaptcha's own execute/verify cycle; only when no
4314
4419
  * widgetId/execute path is available does this fall back to invoking the
4315
- * captured callback directly. Only when no callback is discoverable at all
4316
- * does this fall back to setting the hidden response field directly and
4317
- * dispatching the framework-visible `change` event so the field's own value
4318
- * listener observes it.
4420
+ * captured callback directly. When no callback is discoverable at precheck
4421
+ * time, this re-installs the callback-capture script and retries the lookup
4422
+ * once — catching a widget whose `hcaptcha.render` call happens on demand
4423
+ * rather than on page load, after this frame missed both session.ts's
4424
+ * page-init-script install and the per-frame attach install. Only when the
4425
+ * late retry also finds nothing does this fall back to setting the hidden
4426
+ * response field directly and dispatching the framework-visible `change`
4427
+ * event so the field's own value listener observes it.
4319
4428
  *
4320
4429
  * Widgets of this shape anchor a `[data-sitekey]` element whose
4321
4430
  * `data-callback` attribute names a function on `window`; that callback (not
@@ -4396,6 +4505,48 @@ async function injectCaptchaTokenAndSubmit(target, token, responseField = "h-cap
4396
4505
  await target.evaluate(invokeCallbackExpr).catch(() => undefined);
4397
4506
  return { injected, hasForm, callbackDiscovered };
4398
4507
  }
4508
+ // Belt-and-suspenders: this frame missed session.ts's page-init-script
4509
+ // install and bugfix-001's per-frame attach install (it either attached
4510
+ // before the listener was wired, or never fired an observed attach/
4511
+ // navigate event this run). Installing the capture script now is still
4512
+ // idempotent and still cheap, and it's the only remaining chance to catch
4513
+ // a widget that calls `hcaptcha.render` on demand rather than on load.
4514
+ // Re-run the precheck's callback lookup once more before giving up and
4515
+ // falling through to the raw field-set path below.
4516
+ await target.evaluate((0, captcha_callback_capture_1.buildHcaptchaCallbackCaptureScript)()).catch(() => undefined);
4517
+ const latePrecheckExpr = `(() => {
4518
+ ${findCaptchaCallbackExprSrc()}
4519
+ const sitekeyEl = document.querySelector("[data-sitekey]");
4520
+ return Boolean(__findCaptchaCallback(sitekeyEl));
4521
+ })()`;
4522
+ const lateCallbackDiscovered = await target
4523
+ .evaluate(latePrecheckExpr)
4524
+ .catch(() => false);
4525
+ if (lateCallbackDiscovered) {
4526
+ const invokeLateCallbackExpr = `(() => {
4527
+ ${findCaptchaCallbackExprSrc()}
4528
+ const sitekeyEl = document.querySelector("[data-sitekey]");
4529
+ const found = __findCaptchaCallback(sitekeyEl);
4530
+ if (!found) return;
4531
+ const canExecute =
4532
+ found.kind === "captured" &&
4533
+ found.widgetId !== undefined &&
4534
+ found.widgetId !== null &&
4535
+ window.hcaptcha &&
4536
+ typeof window.hcaptcha.execute === "function";
4537
+ if (canExecute) {
4538
+ window.hcaptcha.execute(found.widgetId);
4539
+ return;
4540
+ }
4541
+ found.invoke(${JSON.stringify(token)});
4542
+ })()`;
4543
+ // Same navigate-mid-evaluate tolerance as the non-late invoke above:
4544
+ // the callback (or execute's own verify cycle) is free to submit the
4545
+ // form synchronously, tearing down the execution context before a
4546
+ // return value marshals.
4547
+ await target.evaluate(invokeLateCallbackExpr).catch(() => undefined);
4548
+ return { injected, hasForm, callbackDiscovered: true };
4549
+ }
4399
4550
  const setValueExpr = `(() => {
4400
4551
  const responseField = ${JSON.stringify(responseField)};
4401
4552
  const token = ${JSON.stringify(token)};
@@ -4508,7 +4659,8 @@ async function submitCaptchaGatedForm(target, responseField = "h-captcha-respons
4508
4659
  */
4509
4660
  async function applySelectValue(page, target, selIdx, value) {
4510
4661
  const setExpr = `((selIdx, value) => {
4511
- const sel = Array.from(document.querySelectorAll("select"))[selIdx];
4662
+ const isOpenerPairedHidden = ${exports.OPENER_PAIRED_HIDDEN_SELECT_EL_EXPR};
4663
+ const sel = Array.from(document.querySelectorAll("select")).filter((s) => !isOpenerPairedHidden(s))[selIdx];
4512
4664
  if (!sel) return { ok: false };
4513
4665
  const desc = Object.getOwnPropertyDescriptor(HTMLSelectElement.prototype, "value");
4514
4666
  if (desc && desc.set) { desc.set.call(sel, value); } else { sel.value = value; }
@@ -4523,7 +4675,8 @@ async function applySelectValue(page, target, selIdx, value) {
4523
4675
  await page.waitForTimeout(SELECT_SETTLE_MS);
4524
4676
  const invalidExpr = `((selIdx) => {
4525
4677
  const isInvalid = ${INVALID_MARKER_EL_EXPR};
4526
- const sel = Array.from(document.querySelectorAll("select"))[selIdx];
4678
+ const isOpenerPairedHidden = ${exports.OPENER_PAIRED_HIDDEN_SELECT_EL_EXPR};
4679
+ const sel = Array.from(document.querySelectorAll("select")).filter((s) => !isOpenerPairedHidden(s))[selIdx];
4527
4680
  if (!sel) return false;
4528
4681
  let node = sel;
4529
4682
  for (let depth = 0; depth < 6 && node; depth++) {
@@ -4561,8 +4714,12 @@ async function trySelectPrimitive(params) {
4561
4714
  const norm = (s) => (s || "").replace(/\\s+/g, " ").trim().toLowerCase();
4562
4715
  const wantOpt = norm(option);
4563
4716
  // All native selects, INCLUDING tabindex=-1 (MuiNativeSelect) which the
4564
- // a11y tree — and therefore Stagehand observe — never surfaces.
4565
- const selects = Array.from(document.querySelectorAll("select"));
4717
+ // a11y tree — and therefore Stagehand observe — never surfaces. Excludes
4718
+ // a design-system combobox opener's paired-but-hidden shadow <select>
4719
+ // (Base Web etc.) so tryPromptSelectorPrimitive gets first refusal on it
4720
+ // instead of this primitive silently writing a value the opener never sees.
4721
+ const isOpenerPairedHidden = ${exports.OPENER_PAIRED_HIDDEN_SELECT_EL_EXPR};
4722
+ const selects = Array.from(document.querySelectorAll("select")).filter((s) => !isOpenerPairedHidden(s));
4566
4723
  if (selects.length === 0) return { selectPresent: false };
4567
4724
  // FAST PATH — deterministic option match: exactly one select has an option
4568
4725
  // matching the flow's answer text. Unambiguous (the option text itself
@@ -4752,7 +4909,8 @@ async function tryFillRequiredSelectsPrimitive(params) {
4752
4909
  // (MUI marks NativeSelect via any of these). Reuses the isUnfilled / selLabelText
4753
4910
  // shape from trySelectPrimitive.
4754
4911
  const enumerateExpr = `(() => {
4755
- const selects = Array.from(document.querySelectorAll("select"));
4912
+ const isOpenerPairedHidden = ${exports.OPENER_PAIRED_HIDDEN_SELECT_EL_EXPR};
4913
+ const selects = Array.from(document.querySelectorAll("select")).filter((s) => !isOpenerPairedHidden(s));
4756
4914
  if (selects.length === 0) return { candidates: [] };
4757
4915
  const selLabelText = (sel) => {
4758
4916
  const parts = [];
@@ -5888,6 +6046,7 @@ async function commitPromptOption(params) {
5888
6046
  const isRequestedLeaf = norm(chosenOption.text) === wantOpt;
5889
6047
  const readbackExpr = `((wIdx, markAttr, valueSel, emptyRxSrc, emptyRxFlags, wantText, isRequestedLeaf) => {
5890
6048
  const isInvalid = ${INVALID_MARKER_EL_EXPR};
6049
+ const isPairedHiddenSelect = ${exports.OPENER_PAIRED_HIDDEN_SELECT_EL_EXPR};
5891
6050
  const norm = (s) => (s || "").replace(/\\s+/g, " ").trim().toLowerCase();
5892
6051
  const buttonValue = ${BUTTON_VALUE_EXPR};
5893
6052
  const emptyRx = new RegExp(emptyRxSrc, emptyRxFlags);
@@ -5910,10 +6069,55 @@ async function commitPromptOption(params) {
5910
6069
  if (node.getAttribute && isInvalid(node)) { stillInvalid = true; break; }
5911
6070
  node = node.parentElement;
5912
6071
  }
6072
+ // Corroborating signal when the opener's own text/activedescendant read is
6073
+ // inconclusive: the paired native <select> Base Web (and similar vendors)
6074
+ // keeps in sync BEHIND the real widget now holds a non-empty CURRENT value
6075
+ // (read-only — never written here) that, when its selected option's own
6076
+ // text is available, also matches the requested option.
6077
+ let hiddenSelectMatches = false;
6078
+ if (!textMatches) {
6079
+ let cnode = w;
6080
+ for (let d = 0; d < ${browser_click_expr_1.MAX_SELECTION_ANCESTOR_DEPTH} && cnode && !hiddenSelectMatches; d++) {
6081
+ const selects = cnode.querySelectorAll ? Array.from(cnode.querySelectorAll("select")) : [];
6082
+ for (const sel of selects) {
6083
+ if (!isPairedHiddenSelect(sel)) continue;
6084
+ // isPairedHiddenSelect only proves SOME opener sits near this select —
6085
+ // in a form with multiple prompt-selector fields sharing an outer
6086
+ // ancestor within MAX_SELECTION_ANCESTOR_DEPTH, that opener can be a
6087
+ // SIBLING widget's, not w's. Mirror OPENER_PAIRED_HIDDEN_SELECT_EL_EXPR's
6088
+ // own climb-then-scan-openers shape, but require w itself to be among
6089
+ // the openers found at the NEAREST level that has any — the same level
6090
+ // isPairedHiddenSelect stopped at — so a neighboring field's
6091
+ // already-committed hidden select never corroborates this widget's commit.
6092
+ let snode = sel.parentElement;
6093
+ let pairedWithW = false;
6094
+ for (let sd = 0; sd < ${browser_click_expr_1.MAX_SELECTION_ANCESTOR_DEPTH} && snode && !pairedWithW; sd++) {
6095
+ const openersHere = snode.querySelectorAll
6096
+ ? Array.from(snode.querySelectorAll(${JSON.stringify(PROMPT_TRIGGER_SELECTORS)})).filter((o) => o !== sel)
6097
+ : [];
6098
+ if (openersHere.length > 0) {
6099
+ pairedWithW = openersHere.includes(w);
6100
+ break;
6101
+ }
6102
+ snode = snode.parentElement;
6103
+ }
6104
+ if (!pairedWithW) continue;
6105
+ const v = (sel.value || "").trim();
6106
+ if (!v) continue;
6107
+ const selOpt = sel.options && sel.selectedIndex >= 0 ? sel.options[sel.selectedIndex] : null;
6108
+ const optText = selOpt ? norm(selOpt.textContent || selOpt.label || "") : "";
6109
+ // A selected option with no visible text (icon-only, value-only markup)
6110
+ // carries no corroborating signal — treating it as a match would credit
6111
+ // ANY selection change, including a different option than requested.
6112
+ if (wantText && optText && optText.includes(wantText)) { hiddenSelectMatches = true; break; }
6113
+ }
6114
+ cnode = cnode.parentElement;
6115
+ }
6116
+ }
5913
6117
  // A category click may leave the widget's aria-invalid unset until final
5914
6118
  // submit, so !stillInvalid alone only counts as success when this click
5915
6119
  // landed on the requested leaf option, not an intermediate category.
5916
- return { ok: textMatches || (isRequestedLeaf && !stillInvalid), id: w.id || "" };
6120
+ return { ok: textMatches || (isRequestedLeaf && !stillInvalid) || hiddenSelectMatches, id: w.id || "" };
5917
6121
  })(${JSON.stringify(chosen.wIdx)}, ${JSON.stringify(PROMPT_WIDGET_MARK_ATTR)}, ${JSON.stringify(PROMPT_VALUE_SELECTORS)}, ${JSON.stringify(PROMPT_EMPTY_VALUE_RX_SRC)}, ${JSON.stringify(PROMPT_EMPTY_VALUE_RX_FLAGS)}, ${JSON.stringify(norm(chosenOption.text))}, ${JSON.stringify(isRequestedLeaf)})`;
5918
6122
  const readback = (await target.evaluate(readbackExpr).catch(() => ({ ok: false, id: "" })));
5919
6123
  if (readback?.ok) {
@@ -6015,12 +6219,14 @@ async function hasUnfilledRequiredControlForStep(target, instruction) {
6015
6219
  // (button/aria-haspopup shapes with no native input/select at all) to
6016
6220
  // the container-widget union already covered.
6017
6221
  const controls = Array.from(document.querySelectorAll(
6018
- "input,select,textarea,[role=combobox],[role=listbox],.bb-custom-select-container,[class*='MultiCheckboxInput']," + triggerSel
6222
+ "input,select,textarea,[role=combobox],[role=listbox]," + triggerSel
6019
6223
  ));
6020
6224
  for (const el of controls) {
6021
6225
  if (!isRequired(el) && !(el.querySelector && el.querySelector("[required],[aria-required=true]"))) {
6022
6226
  // container widgets carry required on an inner element; allow those
6023
- if (!/bb-custom-select-container|MultiCheckboxInput/.test((el.getAttribute && el.getAttribute("class")) || "")) continue;
6227
+ const isTriggerWidget =
6228
+ (el.matches && el.matches(triggerSel)) || (el.querySelector && !!el.querySelector(triggerSel));
6229
+ if (!isTriggerWidget) continue;
6024
6230
  }
6025
6231
  // is it empty/invalid?
6026
6232
  const emptyOrInvalid = isEmptyish(el) || (el.querySelector && !!el.querySelector("[aria-invalid=true]"));
@@ -6281,7 +6487,15 @@ async function verifyDomEffect(target, action, preSelectionState = {}) {
6281
6487
  // ancestor, and — since some resolved xpaths land on a sibling label rather
6282
6488
  // than the combobox itself — the nearest combobox within the same
6283
6489
  // field-group container, for a nested input value, falling back to visible
6284
- // text or an aria label/valuetext.
6490
+ // text or an aria label/valuetext. `combo.textContent`/`el.textContent`
6491
+ // is NEVER read directly: for a composite opener+hidden-`<select>`
6492
+ // widget (role="combobox" on the shared container, resolved xpath
6493
+ // landing on the sibling hidden select) that concatenates EVERY
6494
+ // option's text, so it "matches" whatever the requested option is
6495
+ // regardless of what actually got selected — read only the
6496
+ // COMMITTED signal (aria-activedescendant's referenced node, the
6497
+ // select's own selectedIndex option, or the opener's displayed text
6498
+ // with any nested select/listbox option list stripped out first).
6285
6499
  const exprCustom = `(() => {
6286
6500
  const r = document.evaluate(${JSON.stringify(xpath)}, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
6287
6501
  const el = r.singleNodeValue;
@@ -6294,9 +6508,28 @@ async function verifyDomEffect(target, action, preSelectionState = {}) {
6294
6508
  combo = combo || el;
6295
6509
  const nestedInput = combo.querySelector && combo.querySelector('input');
6296
6510
  const value = (nestedInput && nestedInput.value) || el.value || "";
6297
- const text = (combo.textContent || el.textContent || "").trim();
6511
+ const adid = (combo.getAttribute && combo.getAttribute('aria-activedescendant')) ||
6512
+ (el.getAttribute && el.getAttribute('aria-activedescendant'));
6513
+ const adNode = adid && document.getElementById(adid);
6514
+ const adText = adNode ? (adNode.textContent || "").trim() : "";
6515
+ const selectEl = el.tagName === "SELECT" ? el : (combo.tagName === "SELECT" ? combo : null);
6516
+ const selectedOptText = selectEl && selectEl.selectedIndex >= 0 && selectEl.options[selectEl.selectedIndex]
6517
+ ? (selectEl.options[selectEl.selectedIndex].textContent || "").trim()
6518
+ : "";
6519
+ // If combo itself IS the <select> (role="combobox" placed directly
6520
+ // on the native select rather than a wrapping container),
6521
+ // querySelectorAll('select,...') below only prunes DESCENDANTS —
6522
+ // combo's own concatenated option text survives untouched. Skip
6523
+ // the whole-text fallback entirely for that shape; selectedOptText
6524
+ // above already reads its single committed option.
6525
+ const clone = combo.tagName !== "SELECT" && combo.cloneNode ? combo.cloneNode(true) : null;
6526
+ if (clone && clone.querySelectorAll) {
6527
+ clone.querySelectorAll('select,[role="listbox"]').forEach((n) => n.remove());
6528
+ }
6529
+ const ownText = clone ? (clone.textContent || "").trim() : "";
6298
6530
  const ariaText = (el.getAttribute && (el.getAttribute('aria-label') || el.getAttribute('aria-valuetext'))) || "";
6299
- return { value: String(value).trim(), text: String(text || ariaText).trim() };
6531
+ const text = adText || selectedOptText || ownText || ariaText;
6532
+ return { value: String(value).trim(), text: String(text || "").trim() };
6300
6533
  })()`;
6301
6534
  try {
6302
6535
  const customResult = await target.evaluate(exprCustom);
@@ -6390,7 +6623,17 @@ async function verifyDomEffect(target, action, preSelectionState = {}) {
6390
6623
  // may live on a hidden sibling `<input>`/`<select>` the click never
6391
6624
  // re-renders visibly. Diff that control against its own baseline
6392
6625
  // entry instead.
6393
- return await selectionSiblingCommittedValueChanged(target, xpath, preSelectionState);
6626
+ if (await selectionSiblingCommittedValueChanged(target, xpath, preSelectionState)) {
6627
+ return true;
6628
+ }
6629
+ // No selection fingerprint moved anywhere: the click may still have
6630
+ // genuinely opened a combobox's owned option panel (a two-step
6631
+ // "click to open the dropdown" instruction), which flips only
6632
+ // `aria-expanded` — deliberately excluded from every fingerprint
6633
+ // diff above so a bare accordion/tooltip disclosure is never
6634
+ // credited. This dedicated check additionally requires the opener's
6635
+ // owned panel to now render option content before crediting it.
6636
+ return await comboboxOpenerPanelOpened(target, xpath, preSelectionState);
6394
6637
  }
6395
6638
  const isCheckedNow = await locator.isChecked();
6396
6639
  if (!isCheckedNow)
@@ -7321,7 +7564,12 @@ async function executeStepWithHealing(params) {
7321
7564
  });
7322
7565
  const preCaptchaCaptureIdx = latestCaptureIndex(recentCaptures);
7323
7566
  const injectResult = await injectCaptchaTokenAndSubmit(captchaTarget, solved.token);
7324
- logger.info(`${formatStepPrefix(stepIndex, totalSteps)} captchaGated step: token injected=${injectResult.injected} hasForm=${injectResult.hasForm} callbackDiscovered=${injectResult.callbackDiscovered}`);
7567
+ const registryState = await captchaTarget.evaluate(`(() => {
7568
+ const registry = window[${JSON.stringify(captcha_callback_capture_1.HCAPTCHA_CALLBACK_REGISTRY_GLOBAL)}];
7569
+ if (!registry) return "absent";
7570
+ return Object.keys(registry).length === 0 ? "empty" : "populated";
7571
+ })()`);
7572
+ logger.info(`${formatStepPrefix(stepIndex, totalSteps)} captchaGated step: token injected=${injectResult.injected} hasForm=${injectResult.hasForm} callbackDiscovered=${injectResult.callbackDiscovered} registryState=${registryState}`);
7325
7573
  // Reuse the EXISTING waitForTransitionBody poll (no new captcha-specific
7326
7574
  // poll loop), just with a widened budget so the solve+submit round trip
7327
7575
  // doesn't eat into the plain ADVANCE_TRANSITION_POLL_MS window. A