@enricai/barnacle 1.12.38 → 1.12.40
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/api/schemas/submissions.d.ts +10 -0
- package/dist/api/schemas/submissions.d.ts.map +1 -1
- package/dist/config.d.ts +14 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +3 -0
- package/dist/config.js.map +1 -1
- package/dist/lib/telemetry/reconciliation-record.d.ts +28 -0
- package/dist/lib/telemetry/reconciliation-record.d.ts.map +1 -1
- package/dist/lib/telemetry/reconciliation-record.js +19 -1
- package/dist/lib/telemetry/reconciliation-record.js.map +1 -1
- package/dist/lib/telemetry/run-telemetry.d.ts +15 -0
- package/dist/lib/telemetry/run-telemetry.d.ts.map +1 -1
- package/dist/lib/telemetry/run-telemetry.js +10 -0
- package/dist/lib/telemetry/run-telemetry.js.map +1 -1
- package/dist/lib/telemetry/submission-capture.d.ts +5 -0
- package/dist/lib/telemetry/submission-capture.d.ts.map +1 -1
- package/dist/plugins/loader.d.ts.map +1 -1
- package/dist/plugins/loader.js +35 -0
- package/dist/plugins/loader.js.map +1 -1
- package/dist/scraper/errors.d.ts +39 -6
- package/dist/scraper/errors.d.ts.map +1 -1
- package/dist/scraper/errors.js +51 -7
- package/dist/scraper/errors.js.map +1 -1
- package/dist/scraper/flow-runner.d.ts +99 -0
- package/dist/scraper/flow-runner.d.ts.map +1 -1
- package/dist/scraper/flow-runner.js +290 -15
- package/dist/scraper/flow-runner.js.map +1 -1
- package/dist/scraper/http-client.d.ts.map +1 -1
- package/dist/scraper/http-client.js +40 -3
- package/dist/scraper/http-client.js.map +1 -1
- package/dist/scraper/session-create-limiter.d.ts +8 -0
- package/dist/scraper/session-create-limiter.d.ts.map +1 -0
- package/dist/scraper/session-create-limiter.js +80 -0
- package/dist/scraper/session-create-limiter.js.map +1 -0
- package/dist/scraper/session.d.ts.map +1 -1
- package/dist/scraper/session.js +4 -3
- package/dist/scraper/session.js.map +1 -1
- package/dist/scripts/recon-generate.d.ts +21 -2
- package/dist/scripts/recon-generate.d.ts.map +1 -1
- package/dist/scripts/recon-generate.js +51 -3
- package/dist/scripts/recon-generate.js.map +1 -1
- package/dist/site-plugin.d.ts +10 -0
- package/dist/site-plugin.d.ts.map +1 -1
- 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 = exports.waitForSpaReady = 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;
|
|
@@ -471,6 +473,116 @@ const PROMPT_OPTION_SELECTORS = [
|
|
|
471
473
|
"[data-automation-id='promptOption']",
|
|
472
474
|
"[data-uxi-widget-type='selectinputlistitem']",
|
|
473
475
|
].join(",");
|
|
476
|
+
/**
|
|
477
|
+
* In-page predicate source for `(el) => boolean`: true when `el` is a
|
|
478
|
+
* `<select>` that is a design-system combobox opener's paired-but-hidden
|
|
479
|
+
* shadow control (Base Web `bb-customSelect` and any other vendor matching
|
|
480
|
+
* {@link PROMPT_TRIGGER_SELECTORS}). Two independent signals both have to
|
|
481
|
+
* hold, and each is deliberately technique-agnostic rather than an exact
|
|
482
|
+
* enumeration: (1) hiddenness — `offsetParent === null`, a zero-size rect,
|
|
483
|
+
* `display:none`/`visibility:hidden`/`opacity:0`, off-screen positioning, OR
|
|
484
|
+
* any of the layout-fingerprint checks that generalize across the remaining
|
|
485
|
+
* CSS hiding techniques (`hasTinyFootprint` for the sr-only clip idiom and
|
|
486
|
+
* `transform:scale(0)`'s collapsed rect, a parsed `clip:` rect of zero area,
|
|
487
|
+
* an active `clip-path`, a `scale(0)`/zero `matrix()` transform, or an
|
|
488
|
+
* ancestor whose own box is collapsed to near-zero while clipping overflow)
|
|
489
|
+
* — and (2) opener-pairing — described below — which is what makes the
|
|
490
|
+
* hiddenness check safe to broaden: a false-positive on hiddenness alone
|
|
491
|
+
* (e.g. some other reason a real, standalone select reports a tiny rect)
|
|
492
|
+
* still can't misfire unless that same select also sits paired with a
|
|
493
|
+
* rendered combobox opener, which an ordinary select never does. Signal (2)
|
|
494
|
+
* requires the select's nearby container to also hold a
|
|
495
|
+
* {@link PROMPT_TRIGGER_SELECTORS} opener element, other than itself, that is
|
|
496
|
+
* ARIA-wired to a listbox panel — via `aria-owns`/`aria-controls`/
|
|
497
|
+
* `aria-haspopup="listbox"` declared on the opener (accepted unresolved only
|
|
498
|
+
* when the opener sits in the select's own immediate container, since many
|
|
499
|
+
* widgets mount their popup lazily on first open and haven't rendered it
|
|
500
|
+
* yet), or, once the referenced id does resolve (at any climbed depth), only
|
|
501
|
+
* when the target actually is `role=listbox` or contains a
|
|
502
|
+
* {@link PROMPT_OPTION_SELECTORS} match, or, for an opener declaring no
|
|
503
|
+
* owns/controls/haspopup at all, a {@link PROMPT_OPTION_SELECTORS} match
|
|
504
|
+
* rendered in THAT OPENER's own immediate container (never the whole climbed
|
|
505
|
+
* ancestor — scanning the ancestor would credit an unrelated already-open
|
|
506
|
+
* widget elsewhere in a shared ancestor as this select's pairing). Requiring
|
|
507
|
+
* resolution past the immediate container keeps the broadened hidden test
|
|
508
|
+
* from misclassifying an unrelated widget elsewhere in a shared ancestor as
|
|
509
|
+
* a paired shadow select.
|
|
510
|
+
* The bounded-ancestor-climb-then-subtree-search shape mirrors
|
|
511
|
+
* {@link NEARBY_SELECTION_CONTAINER_FN_SRC}, but searches for
|
|
512
|
+
* {@link PROMPT_TRIGGER_SELECTORS} instead of the selection-marker union —
|
|
513
|
+
* this ties "opener" to the exact same union `tryPromptSelectorPrimitive`
|
|
514
|
+
* recognizes, so a select is excluded precisely when the primitive that
|
|
515
|
+
* should own it would actually claim it. Wired into every `<select>` list
|
|
516
|
+
* `trySelectPrimitive` and `applySelectValue` build, so those primitives fall
|
|
517
|
+
* through and let `tryPromptSelectorPrimitive` claim the opener instead — and
|
|
518
|
+
* consumed by {@link commitPromptOption}'s hidden-select-value corroboration
|
|
519
|
+
* when the opener's own readback is inconclusive.
|
|
520
|
+
*/
|
|
521
|
+
exports.OPENER_PAIRED_HIDDEN_SELECT_EL_EXPR = `(el) => {
|
|
522
|
+
if (!el || el.tagName !== "SELECT") return false;
|
|
523
|
+
const rect = el.getBoundingClientRect();
|
|
524
|
+
const style = getComputedStyle(el);
|
|
525
|
+
const isOffscreen = rect.right <= 0 || rect.bottom <= 0 || rect.left >= window.innerWidth || rect.top >= window.innerHeight;
|
|
526
|
+
// A near-zero footprint (<=1px either dimension) is the layout-level fingerprint
|
|
527
|
+
// shared by the classic sr-only clip technique (explicit width:1px;height:1px) and
|
|
528
|
+
// a real browser's transform:scale(0) (which collapses the bounding rect itself) —
|
|
529
|
+
// technique-agnostic, unlike enumerating every CSS property that can shrink a box.
|
|
530
|
+
const hasTinyFootprint = rect.width <= 1 || rect.height <= 1;
|
|
531
|
+
const clipMatch = /rect\\(\\s*([\\d.]+)px[,\\s]+([\\d.]+)px[,\\s]+([\\d.]+)px[,\\s]+([\\d.]+)px\\s*\\)/.exec(style.clip || "");
|
|
532
|
+
const isClipZeroArea = !!clipMatch && (parseFloat(clipMatch[3]) - parseFloat(clipMatch[1]) <= 0 || parseFloat(clipMatch[2]) - parseFloat(clipMatch[4]) <= 0);
|
|
533
|
+
// Only treat clip-path as a hiding signal when it collapses the element to
|
|
534
|
+
// zero visible area (circle(0), inset(50%), a degenerate polygon whose
|
|
535
|
+
// points share a single coordinate) — a decorative clip-path (rounded
|
|
536
|
+
// corners, an avatar circle) leaves the element fully visible and must
|
|
537
|
+
// not be misread as hidden.
|
|
538
|
+
const cp = style.clipPath || "";
|
|
539
|
+
const hasClipPath = /circle\\(\\s*0(?:px|%)?\\s*(?:at\\s[^)]*)?\\)/i.test(cp) || /ellipse\\(\\s*0(?:px|%)?\\s+0(?:px|%)?\\s*(?:at\\s[^)]*)?\\)/i.test(cp) || /inset\\(\\s*50%\\s*\\)/i.test(cp) || /polygon\\(\\s*(?:0%?\\s+0%?\\s*,?\\s*)+\\)/i.test(cp);
|
|
540
|
+
const hasZeroScale = /scale\\(\\s*0(?:\\.0+)?\\s*(?:,\\s*0(?:\\.0+)?\\s*)?\\)/.test(style.transform || "") || /matrix\\(\\s*0\\s*,\\s*0\\s*,\\s*0\\s*,\\s*0\\s*,/.test(style.transform || "");
|
|
541
|
+
// Ancestor overflow-clipping: a wrapper with overflow:hidden collapsed to a
|
|
542
|
+
// near-zero box clips its whole subtree to invisibility even though the
|
|
543
|
+
// select's own box, style and offsetParent all look ordinary.
|
|
544
|
+
const isAncestorClipped = (() => {
|
|
545
|
+
let anc = el.parentElement;
|
|
546
|
+
for (let d = 0; d < ${browser_click_expr_1.MAX_SELECTION_ANCESTOR_DEPTH} && anc; d++) {
|
|
547
|
+
const ancStyle = getComputedStyle(anc);
|
|
548
|
+
const overflowHides = ancStyle.overflow === "hidden" || ancStyle.overflow === "clip" || ancStyle.overflowX === "hidden" || ancStyle.overflowY === "hidden";
|
|
549
|
+
if (overflowHides) {
|
|
550
|
+
const ancRect = anc.getBoundingClientRect();
|
|
551
|
+
if (ancRect.width <= 1 || ancRect.height <= 1) return true;
|
|
552
|
+
}
|
|
553
|
+
anc = anc.parentElement;
|
|
554
|
+
}
|
|
555
|
+
return false;
|
|
556
|
+
})();
|
|
557
|
+
const isHidden = el.offsetParent === null || (rect.width === 0 && rect.height === 0) || style.display === "none" || style.visibility === "hidden" || parseFloat(style.opacity) === 0 || isOffscreen || hasTinyFootprint || isClipZeroArea || hasClipPath || hasZeroScale || isAncestorClipped;
|
|
558
|
+
if (!isHidden) return false;
|
|
559
|
+
const triggerSel = ${JSON.stringify(PROMPT_TRIGGER_SELECTORS)};
|
|
560
|
+
const optionSel = ${JSON.stringify(PROMPT_OPTION_SELECTORS)};
|
|
561
|
+
const hasRenderedPanel = (opener, allowUnresolvedDeclaration) => {
|
|
562
|
+
const ownsId = opener.getAttribute("aria-owns") || opener.getAttribute("aria-controls");
|
|
563
|
+
const target = ownsId ? document.getElementById(ownsId) : null;
|
|
564
|
+
if (target) return target.getAttribute("role") === "listbox" || target.querySelector(optionSel) !== null;
|
|
565
|
+
if (!allowUnresolvedDeclaration) return false;
|
|
566
|
+
if (!!ownsId || opener.getAttribute("aria-haspopup") === "listbox") return true;
|
|
567
|
+
return !!(opener.parentElement && opener.parentElement.querySelector(optionSel));
|
|
568
|
+
};
|
|
569
|
+
let node = el.parentElement;
|
|
570
|
+
for (let depth = 0; depth < ${browser_click_expr_1.MAX_SELECTION_ANCESTOR_DEPTH} && node; depth++) {
|
|
571
|
+
const openers = node.querySelectorAll ? Array.from(node.querySelectorAll(triggerSel)) : [];
|
|
572
|
+
// Only the select's own immediate container may pair on a merely-declared
|
|
573
|
+
// (not-yet-rendered) aria-owns/aria-controls/aria-haspopup target — an
|
|
574
|
+
// opener found only by climbing further up must have its panel actually
|
|
575
|
+
// resolve, or an unrelated sibling widget elsewhere in a shared ancestor
|
|
576
|
+
// (e.g. a distant visible field's own opener) would false-positive. An
|
|
577
|
+
// opener with no ARIA pairing declaration at all is credited only when
|
|
578
|
+
// an option-shaped element sits in ITS OWN immediate container — never
|
|
579
|
+
// by scanning the whole climbed ancestor, which would credit an
|
|
580
|
+
// unrelated already-open widget elsewhere in a shared ancestor.
|
|
581
|
+
if (openers.some((opener) => opener !== el && hasRenderedPanel(opener, depth === 0))) return true;
|
|
582
|
+
node = node.parentElement;
|
|
583
|
+
}
|
|
584
|
+
return false;
|
|
585
|
+
}`;
|
|
474
586
|
/**
|
|
475
587
|
* Cross-vendor selector union for an in-popup filter/typeahead input — standards
|
|
476
588
|
* first, then the widget-kit's bare filter `<input>` (which carries no
|
|
@@ -3060,7 +3172,7 @@ const XPATH_OF_FN_SRC = `(node) => {
|
|
|
3060
3172
|
* popover isn't mistaken for a selection.
|
|
3061
3173
|
*/
|
|
3062
3174
|
function selectionFingerprintObjSrc(elVar, dsVar) {
|
|
3063
|
-
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) : "" }`;
|
|
3175
|
+
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") || "" }`;
|
|
3064
3176
|
}
|
|
3065
3177
|
/**
|
|
3066
3178
|
* Browser-side expression body that resolves `xpath` and returns that one
|
|
@@ -3145,7 +3257,7 @@ const SELECTION_STATE_MAP_EXPR = `(() => {
|
|
|
3145
3257
|
const style = getComputedStyle(el);
|
|
3146
3258
|
return style.display !== "none" && style.visibility !== "hidden";
|
|
3147
3259
|
};
|
|
3148
|
-
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)};
|
|
3260
|
+
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)};
|
|
3149
3261
|
const skip = (el) => el.closest("[role=dialog],[role=tooltip],[aria-live]") !== null;
|
|
3150
3262
|
const isCommittedValueControl = (el) =>
|
|
3151
3263
|
(el.tagName === "INPUT" || el.tagName === "SELECT") &&
|
|
@@ -3210,6 +3322,7 @@ function asSelectionFingerprint(raw) {
|
|
|
3210
3322
|
dataChecked: typeof r.dataChecked === "string" ? r.dataChecked : "",
|
|
3211
3323
|
checked: typeof r.checked === "string" ? r.checked : "",
|
|
3212
3324
|
value: typeof r.value === "string" ? r.value : "",
|
|
3325
|
+
ariaExpanded: typeof r.ariaExpanded === "string" ? r.ariaExpanded : "",
|
|
3213
3326
|
};
|
|
3214
3327
|
}
|
|
3215
3328
|
/**
|
|
@@ -3406,6 +3519,76 @@ async function selectionSiblingCommittedValueChanged(target, leafXpath, preSelec
|
|
|
3406
3519
|
return false;
|
|
3407
3520
|
}
|
|
3408
3521
|
}
|
|
3522
|
+
/**
|
|
3523
|
+
* Dedicated pre/post check crediting exactly one case none of the fingerprint
|
|
3524
|
+
* diffs above can see: a raw click resolved onto (or under) a
|
|
3525
|
+
* {@link PROMPT_TRIGGER_SELECTORS}-shaped combobox opener that GENUINELY opened
|
|
3526
|
+
* its owned option panel. Opening a widget only flips `aria-expanded`, which
|
|
3527
|
+
* {@link selectionFingerprintObjSrc}'s shared fingerprint deliberately blanks out
|
|
3528
|
+
* of `dataState` and which {@link selectionFingerprintChanged} /
|
|
3529
|
+
* {@link selectionAncestorChanged}'s `changed()` never compare — by design, so a
|
|
3530
|
+
* bare accordion/tooltip disclosure is never mistaken for a selection. That
|
|
3531
|
+
* leaves a replanner-generated two-step "click to open the dropdown"
|
|
3532
|
+
* instruction with no signal at all: the fingerprint tiers above never move,
|
|
3533
|
+
* so the open-click is scored phantom even though it genuinely exposed the
|
|
3534
|
+
* option panel.
|
|
3535
|
+
*
|
|
3536
|
+
* This function is independent of those tiers rather than folded into their
|
|
3537
|
+
* `changed()`: it reads the new {@link ElementSelectionFingerprint.ariaExpanded}
|
|
3538
|
+
* field, but ONLY on a node that itself matches {@link PROMPT_TRIGGER_SELECTORS},
|
|
3539
|
+
* and ONLY credits it when the opener's `aria-owns`/`aria-controls` scope (the
|
|
3540
|
+
* SAME {@link PROMPT_SCOPE_ROOT_EXPR} resolution `tryPromptSelectorPrimitive`
|
|
3541
|
+
* uses) now actually contains {@link PROMPT_OPTION_SELECTORS} content — never a
|
|
3542
|
+
* bare `aria-expanded` flip with nothing rendered underneath. That second
|
|
3543
|
+
* condition is what keeps a bare accordion/tooltip trigger (which flips
|
|
3544
|
+
* `aria-expanded` but owns no option panel) excluded, preserving the existing
|
|
3545
|
+
* deliberate exclusion.
|
|
3546
|
+
*
|
|
3547
|
+
* Walks from the leaf up to {@link MAX_SELECTION_ANCESTOR_DEPTH} for the NEAREST
|
|
3548
|
+
* trigger match (mirrors {@link selectionAncestorChanged}'s climb, since
|
|
3549
|
+
* Stagehand may resolve the click onto a label/icon child of the opener rather
|
|
3550
|
+
* than the opener itself). No baseline entry for that trigger, no trigger
|
|
3551
|
+
* match, a trigger that was already expanded pre-click, or a trigger that's
|
|
3552
|
+
* still collapsed post-click → `false` (defer to the network/URL signal).
|
|
3553
|
+
* Returns `false` on any evaluate throw.
|
|
3554
|
+
*/
|
|
3555
|
+
async function comboboxOpenerPanelOpened(target, leafXpath, preSelectionState) {
|
|
3556
|
+
const expr = `(() => {
|
|
3557
|
+
const LEAF = ${JSON.stringify(leafXpath)};
|
|
3558
|
+
const BASE = ${JSON.stringify(preSelectionState)};
|
|
3559
|
+
const TRIGGER_SEL = ${JSON.stringify(PROMPT_TRIGGER_SELECTORS)};
|
|
3560
|
+
const OPTION_SEL = ${JSON.stringify(PROMPT_OPTION_SELECTORS)};
|
|
3561
|
+
const xpathOf = ${XPATH_OF_FN_SRC};
|
|
3562
|
+
const scopeRootOf = ${PROMPT_SCOPE_ROOT_EXPR};
|
|
3563
|
+
const r = document.evaluate(LEAF, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
|
|
3564
|
+
let node = r.singleNodeValue;
|
|
3565
|
+
for (let depth = 0; depth < ${browser_click_expr_1.MAX_SELECTION_ANCESTOR_DEPTH} && node; depth++) {
|
|
3566
|
+
if (node.getAttribute && node.matches && node.matches(TRIGGER_SEL)) {
|
|
3567
|
+
const pre = BASE[xpathOf(node)];
|
|
3568
|
+
if (!pre) return false;
|
|
3569
|
+
const wasCollapsed = pre.ariaExpanded !== "true";
|
|
3570
|
+
const nowExpanded = (node.getAttribute("aria-expanded") || "") === "true";
|
|
3571
|
+
if (!wasCollapsed || !nowExpanded) return false;
|
|
3572
|
+
// scopeRootOf falls back to \`document\` when the trigger has no
|
|
3573
|
+
// aria-controls/aria-owns ref AND no inline popup in its own subtree.
|
|
3574
|
+
// That fallback is a page-wide search, not the opener's OWNED scope —
|
|
3575
|
+
// crediting through it would let an unrelated widget's already-open
|
|
3576
|
+
// options (elsewhere on the page) falsely credit THIS trigger's click.
|
|
3577
|
+
const scope = scopeRootOf(node);
|
|
3578
|
+
if (!scope || scope === document || !scope.querySelector) return false;
|
|
3579
|
+
return !!scope.querySelector(OPTION_SEL);
|
|
3580
|
+
}
|
|
3581
|
+
node = node.parentElement;
|
|
3582
|
+
}
|
|
3583
|
+
return false;
|
|
3584
|
+
})()`;
|
|
3585
|
+
try {
|
|
3586
|
+
return (await target.evaluate(expr)) === true;
|
|
3587
|
+
}
|
|
3588
|
+
catch {
|
|
3589
|
+
return false;
|
|
3590
|
+
}
|
|
3591
|
+
}
|
|
3409
3592
|
/** How long the upload primitive waits for a post-setInputFiles network POST. */
|
|
3410
3593
|
const UPLOAD_NETWORK_TIMEOUT_MS = 5_000;
|
|
3411
3594
|
/** Polling interval while waiting for the upload's network signal. */
|
|
@@ -4556,7 +4739,8 @@ async function submitCaptchaGatedForm(target, responseField = "h-captcha-respons
|
|
|
4556
4739
|
*/
|
|
4557
4740
|
async function applySelectValue(page, target, selIdx, value) {
|
|
4558
4741
|
const setExpr = `((selIdx, value) => {
|
|
4559
|
-
const
|
|
4742
|
+
const isOpenerPairedHidden = ${exports.OPENER_PAIRED_HIDDEN_SELECT_EL_EXPR};
|
|
4743
|
+
const sel = Array.from(document.querySelectorAll("select")).filter((s) => !isOpenerPairedHidden(s))[selIdx];
|
|
4560
4744
|
if (!sel) return { ok: false };
|
|
4561
4745
|
const desc = Object.getOwnPropertyDescriptor(HTMLSelectElement.prototype, "value");
|
|
4562
4746
|
if (desc && desc.set) { desc.set.call(sel, value); } else { sel.value = value; }
|
|
@@ -4571,7 +4755,8 @@ async function applySelectValue(page, target, selIdx, value) {
|
|
|
4571
4755
|
await page.waitForTimeout(SELECT_SETTLE_MS);
|
|
4572
4756
|
const invalidExpr = `((selIdx) => {
|
|
4573
4757
|
const isInvalid = ${INVALID_MARKER_EL_EXPR};
|
|
4574
|
-
const
|
|
4758
|
+
const isOpenerPairedHidden = ${exports.OPENER_PAIRED_HIDDEN_SELECT_EL_EXPR};
|
|
4759
|
+
const sel = Array.from(document.querySelectorAll("select")).filter((s) => !isOpenerPairedHidden(s))[selIdx];
|
|
4575
4760
|
if (!sel) return false;
|
|
4576
4761
|
let node = sel;
|
|
4577
4762
|
for (let depth = 0; depth < 6 && node; depth++) {
|
|
@@ -4609,8 +4794,12 @@ async function trySelectPrimitive(params) {
|
|
|
4609
4794
|
const norm = (s) => (s || "").replace(/\\s+/g, " ").trim().toLowerCase();
|
|
4610
4795
|
const wantOpt = norm(option);
|
|
4611
4796
|
// All native selects, INCLUDING tabindex=-1 (MuiNativeSelect) which the
|
|
4612
|
-
// a11y tree — and therefore Stagehand observe — never surfaces.
|
|
4613
|
-
|
|
4797
|
+
// a11y tree — and therefore Stagehand observe — never surfaces. Excludes
|
|
4798
|
+
// a design-system combobox opener's paired-but-hidden shadow <select>
|
|
4799
|
+
// (Base Web etc.) so tryPromptSelectorPrimitive gets first refusal on it
|
|
4800
|
+
// instead of this primitive silently writing a value the opener never sees.
|
|
4801
|
+
const isOpenerPairedHidden = ${exports.OPENER_PAIRED_HIDDEN_SELECT_EL_EXPR};
|
|
4802
|
+
const selects = Array.from(document.querySelectorAll("select")).filter((s) => !isOpenerPairedHidden(s));
|
|
4614
4803
|
if (selects.length === 0) return { selectPresent: false };
|
|
4615
4804
|
// FAST PATH — deterministic option match: exactly one select has an option
|
|
4616
4805
|
// matching the flow's answer text. Unambiguous (the option text itself
|
|
@@ -4800,7 +4989,8 @@ async function tryFillRequiredSelectsPrimitive(params) {
|
|
|
4800
4989
|
// (MUI marks NativeSelect via any of these). Reuses the isUnfilled / selLabelText
|
|
4801
4990
|
// shape from trySelectPrimitive.
|
|
4802
4991
|
const enumerateExpr = `(() => {
|
|
4803
|
-
const
|
|
4992
|
+
const isOpenerPairedHidden = ${exports.OPENER_PAIRED_HIDDEN_SELECT_EL_EXPR};
|
|
4993
|
+
const selects = Array.from(document.querySelectorAll("select")).filter((s) => !isOpenerPairedHidden(s));
|
|
4804
4994
|
if (selects.length === 0) return { candidates: [] };
|
|
4805
4995
|
const selLabelText = (sel) => {
|
|
4806
4996
|
const parts = [];
|
|
@@ -5936,6 +6126,7 @@ async function commitPromptOption(params) {
|
|
|
5936
6126
|
const isRequestedLeaf = norm(chosenOption.text) === wantOpt;
|
|
5937
6127
|
const readbackExpr = `((wIdx, markAttr, valueSel, emptyRxSrc, emptyRxFlags, wantText, isRequestedLeaf) => {
|
|
5938
6128
|
const isInvalid = ${INVALID_MARKER_EL_EXPR};
|
|
6129
|
+
const isPairedHiddenSelect = ${exports.OPENER_PAIRED_HIDDEN_SELECT_EL_EXPR};
|
|
5939
6130
|
const norm = (s) => (s || "").replace(/\\s+/g, " ").trim().toLowerCase();
|
|
5940
6131
|
const buttonValue = ${BUTTON_VALUE_EXPR};
|
|
5941
6132
|
const emptyRx = new RegExp(emptyRxSrc, emptyRxFlags);
|
|
@@ -5958,10 +6149,55 @@ async function commitPromptOption(params) {
|
|
|
5958
6149
|
if (node.getAttribute && isInvalid(node)) { stillInvalid = true; break; }
|
|
5959
6150
|
node = node.parentElement;
|
|
5960
6151
|
}
|
|
6152
|
+
// Corroborating signal when the opener's own text/activedescendant read is
|
|
6153
|
+
// inconclusive: the paired native <select> Base Web (and similar vendors)
|
|
6154
|
+
// keeps in sync BEHIND the real widget now holds a non-empty CURRENT value
|
|
6155
|
+
// (read-only — never written here) that, when its selected option's own
|
|
6156
|
+
// text is available, also matches the requested option.
|
|
6157
|
+
let hiddenSelectMatches = false;
|
|
6158
|
+
if (!textMatches) {
|
|
6159
|
+
let cnode = w;
|
|
6160
|
+
for (let d = 0; d < ${browser_click_expr_1.MAX_SELECTION_ANCESTOR_DEPTH} && cnode && !hiddenSelectMatches; d++) {
|
|
6161
|
+
const selects = cnode.querySelectorAll ? Array.from(cnode.querySelectorAll("select")) : [];
|
|
6162
|
+
for (const sel of selects) {
|
|
6163
|
+
if (!isPairedHiddenSelect(sel)) continue;
|
|
6164
|
+
// isPairedHiddenSelect only proves SOME opener sits near this select —
|
|
6165
|
+
// in a form with multiple prompt-selector fields sharing an outer
|
|
6166
|
+
// ancestor within MAX_SELECTION_ANCESTOR_DEPTH, that opener can be a
|
|
6167
|
+
// SIBLING widget's, not w's. Mirror OPENER_PAIRED_HIDDEN_SELECT_EL_EXPR's
|
|
6168
|
+
// own climb-then-scan-openers shape, but require w itself to be among
|
|
6169
|
+
// the openers found at the NEAREST level that has any — the same level
|
|
6170
|
+
// isPairedHiddenSelect stopped at — so a neighboring field's
|
|
6171
|
+
// already-committed hidden select never corroborates this widget's commit.
|
|
6172
|
+
let snode = sel.parentElement;
|
|
6173
|
+
let pairedWithW = false;
|
|
6174
|
+
for (let sd = 0; sd < ${browser_click_expr_1.MAX_SELECTION_ANCESTOR_DEPTH} && snode && !pairedWithW; sd++) {
|
|
6175
|
+
const openersHere = snode.querySelectorAll
|
|
6176
|
+
? Array.from(snode.querySelectorAll(${JSON.stringify(PROMPT_TRIGGER_SELECTORS)})).filter((o) => o !== sel)
|
|
6177
|
+
: [];
|
|
6178
|
+
if (openersHere.length > 0) {
|
|
6179
|
+
pairedWithW = openersHere.includes(w);
|
|
6180
|
+
break;
|
|
6181
|
+
}
|
|
6182
|
+
snode = snode.parentElement;
|
|
6183
|
+
}
|
|
6184
|
+
if (!pairedWithW) continue;
|
|
6185
|
+
const v = (sel.value || "").trim();
|
|
6186
|
+
if (!v) continue;
|
|
6187
|
+
const selOpt = sel.options && sel.selectedIndex >= 0 ? sel.options[sel.selectedIndex] : null;
|
|
6188
|
+
const optText = selOpt ? norm(selOpt.textContent || selOpt.label || "") : "";
|
|
6189
|
+
// A selected option with no visible text (icon-only, value-only markup)
|
|
6190
|
+
// carries no corroborating signal — treating it as a match would credit
|
|
6191
|
+
// ANY selection change, including a different option than requested.
|
|
6192
|
+
if (wantText && optText && optText.includes(wantText)) { hiddenSelectMatches = true; break; }
|
|
6193
|
+
}
|
|
6194
|
+
cnode = cnode.parentElement;
|
|
6195
|
+
}
|
|
6196
|
+
}
|
|
5961
6197
|
// A category click may leave the widget's aria-invalid unset until final
|
|
5962
6198
|
// submit, so !stillInvalid alone only counts as success when this click
|
|
5963
6199
|
// landed on the requested leaf option, not an intermediate category.
|
|
5964
|
-
return { ok: textMatches || (isRequestedLeaf && !stillInvalid), id: w.id || "" };
|
|
6200
|
+
return { ok: textMatches || (isRequestedLeaf && !stillInvalid) || hiddenSelectMatches, id: w.id || "" };
|
|
5965
6201
|
})(${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)})`;
|
|
5966
6202
|
const readback = (await target.evaluate(readbackExpr).catch(() => ({ ok: false, id: "" })));
|
|
5967
6203
|
if (readback?.ok) {
|
|
@@ -6063,12 +6299,14 @@ async function hasUnfilledRequiredControlForStep(target, instruction) {
|
|
|
6063
6299
|
// (button/aria-haspopup shapes with no native input/select at all) to
|
|
6064
6300
|
// the container-widget union already covered.
|
|
6065
6301
|
const controls = Array.from(document.querySelectorAll(
|
|
6066
|
-
"input,select,textarea,[role=combobox],[role=listbox]
|
|
6302
|
+
"input,select,textarea,[role=combobox],[role=listbox]," + triggerSel
|
|
6067
6303
|
));
|
|
6068
6304
|
for (const el of controls) {
|
|
6069
6305
|
if (!isRequired(el) && !(el.querySelector && el.querySelector("[required],[aria-required=true]"))) {
|
|
6070
6306
|
// container widgets carry required on an inner element; allow those
|
|
6071
|
-
|
|
6307
|
+
const isTriggerWidget =
|
|
6308
|
+
(el.matches && el.matches(triggerSel)) || (el.querySelector && !!el.querySelector(triggerSel));
|
|
6309
|
+
if (!isTriggerWidget) continue;
|
|
6072
6310
|
}
|
|
6073
6311
|
// is it empty/invalid?
|
|
6074
6312
|
const emptyOrInvalid = isEmptyish(el) || (el.querySelector && !!el.querySelector("[aria-invalid=true]"));
|
|
@@ -6329,7 +6567,15 @@ async function verifyDomEffect(target, action, preSelectionState = {}) {
|
|
|
6329
6567
|
// ancestor, and — since some resolved xpaths land on a sibling label rather
|
|
6330
6568
|
// than the combobox itself — the nearest combobox within the same
|
|
6331
6569
|
// field-group container, for a nested input value, falling back to visible
|
|
6332
|
-
// text or an aria label/valuetext.
|
|
6570
|
+
// text or an aria label/valuetext. `combo.textContent`/`el.textContent`
|
|
6571
|
+
// is NEVER read directly: for a composite opener+hidden-`<select>`
|
|
6572
|
+
// widget (role="combobox" on the shared container, resolved xpath
|
|
6573
|
+
// landing on the sibling hidden select) that concatenates EVERY
|
|
6574
|
+
// option's text, so it "matches" whatever the requested option is
|
|
6575
|
+
// regardless of what actually got selected — read only the
|
|
6576
|
+
// COMMITTED signal (aria-activedescendant's referenced node, the
|
|
6577
|
+
// select's own selectedIndex option, or the opener's displayed text
|
|
6578
|
+
// with any nested select/listbox option list stripped out first).
|
|
6333
6579
|
const exprCustom = `(() => {
|
|
6334
6580
|
const r = document.evaluate(${JSON.stringify(xpath)}, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
|
|
6335
6581
|
const el = r.singleNodeValue;
|
|
@@ -6342,9 +6588,28 @@ async function verifyDomEffect(target, action, preSelectionState = {}) {
|
|
|
6342
6588
|
combo = combo || el;
|
|
6343
6589
|
const nestedInput = combo.querySelector && combo.querySelector('input');
|
|
6344
6590
|
const value = (nestedInput && nestedInput.value) || el.value || "";
|
|
6345
|
-
const
|
|
6591
|
+
const adid = (combo.getAttribute && combo.getAttribute('aria-activedescendant')) ||
|
|
6592
|
+
(el.getAttribute && el.getAttribute('aria-activedescendant'));
|
|
6593
|
+
const adNode = adid && document.getElementById(adid);
|
|
6594
|
+
const adText = adNode ? (adNode.textContent || "").trim() : "";
|
|
6595
|
+
const selectEl = el.tagName === "SELECT" ? el : (combo.tagName === "SELECT" ? combo : null);
|
|
6596
|
+
const selectedOptText = selectEl && selectEl.selectedIndex >= 0 && selectEl.options[selectEl.selectedIndex]
|
|
6597
|
+
? (selectEl.options[selectEl.selectedIndex].textContent || "").trim()
|
|
6598
|
+
: "";
|
|
6599
|
+
// If combo itself IS the <select> (role="combobox" placed directly
|
|
6600
|
+
// on the native select rather than a wrapping container),
|
|
6601
|
+
// querySelectorAll('select,...') below only prunes DESCENDANTS —
|
|
6602
|
+
// combo's own concatenated option text survives untouched. Skip
|
|
6603
|
+
// the whole-text fallback entirely for that shape; selectedOptText
|
|
6604
|
+
// above already reads its single committed option.
|
|
6605
|
+
const clone = combo.tagName !== "SELECT" && combo.cloneNode ? combo.cloneNode(true) : null;
|
|
6606
|
+
if (clone && clone.querySelectorAll) {
|
|
6607
|
+
clone.querySelectorAll('select,[role="listbox"]').forEach((n) => n.remove());
|
|
6608
|
+
}
|
|
6609
|
+
const ownText = clone ? (clone.textContent || "").trim() : "";
|
|
6346
6610
|
const ariaText = (el.getAttribute && (el.getAttribute('aria-label') || el.getAttribute('aria-valuetext'))) || "";
|
|
6347
|
-
|
|
6611
|
+
const text = adText || selectedOptText || ownText || ariaText;
|
|
6612
|
+
return { value: String(value).trim(), text: String(text || "").trim() };
|
|
6348
6613
|
})()`;
|
|
6349
6614
|
try {
|
|
6350
6615
|
const customResult = await target.evaluate(exprCustom);
|
|
@@ -6438,7 +6703,17 @@ async function verifyDomEffect(target, action, preSelectionState = {}) {
|
|
|
6438
6703
|
// may live on a hidden sibling `<input>`/`<select>` the click never
|
|
6439
6704
|
// re-renders visibly. Diff that control against its own baseline
|
|
6440
6705
|
// entry instead.
|
|
6441
|
-
|
|
6706
|
+
if (await selectionSiblingCommittedValueChanged(target, xpath, preSelectionState)) {
|
|
6707
|
+
return true;
|
|
6708
|
+
}
|
|
6709
|
+
// No selection fingerprint moved anywhere: the click may still have
|
|
6710
|
+
// genuinely opened a combobox's owned option panel (a two-step
|
|
6711
|
+
// "click to open the dropdown" instruction), which flips only
|
|
6712
|
+
// `aria-expanded` — deliberately excluded from every fingerprint
|
|
6713
|
+
// diff above so a bare accordion/tooltip disclosure is never
|
|
6714
|
+
// credited. This dedicated check additionally requires the opener's
|
|
6715
|
+
// owned panel to now render option content before crediting it.
|
|
6716
|
+
return await comboboxOpenerPanelOpened(target, xpath, preSelectionState);
|
|
6442
6717
|
}
|
|
6443
6718
|
const isCheckedNow = await locator.isChecked();
|
|
6444
6719
|
if (!isCheckedNow)
|