@enricai/barnacle 1.12.38 → 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.
- 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 +73 -0
- package/dist/scraper/flow-runner.d.ts.map +1 -1
- package/dist/scraper/flow-runner.js +210 -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 +16 -2
- package/dist/scripts/recon-generate.d.ts.map +1 -1
- package/dist/scripts/recon-generate.js +23 -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;
|
|
@@ -461,6 +463,36 @@ const PROMPT_TRIGGER_SELECTORS = [
|
|
|
461
463
|
"[data-automation-id='multiSelectContainer']",
|
|
462
464
|
"[data-automation-id='promptSelectionLabel']",
|
|
463
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
|
+
}`;
|
|
464
496
|
/**
|
|
465
497
|
* Cross-vendor selector union for an OPTION rendered inside the opened popup —
|
|
466
498
|
* standards first (`role=option`), then the widget-kit option markers. Sibling
|
|
@@ -3060,7 +3092,7 @@ const XPATH_OF_FN_SRC = `(node) => {
|
|
|
3060
3092
|
* popover isn't mistaken for a selection.
|
|
3061
3093
|
*/
|
|
3062
3094
|
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) : "" }`;
|
|
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") || "" }`;
|
|
3064
3096
|
}
|
|
3065
3097
|
/**
|
|
3066
3098
|
* Browser-side expression body that resolves `xpath` and returns that one
|
|
@@ -3145,7 +3177,7 @@ const SELECTION_STATE_MAP_EXPR = `(() => {
|
|
|
3145
3177
|
const style = getComputedStyle(el);
|
|
3146
3178
|
return style.display !== "none" && style.visibility !== "hidden";
|
|
3147
3179
|
};
|
|
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)};
|
|
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)};
|
|
3149
3181
|
const skip = (el) => el.closest("[role=dialog],[role=tooltip],[aria-live]") !== null;
|
|
3150
3182
|
const isCommittedValueControl = (el) =>
|
|
3151
3183
|
(el.tagName === "INPUT" || el.tagName === "SELECT") &&
|
|
@@ -3210,6 +3242,7 @@ function asSelectionFingerprint(raw) {
|
|
|
3210
3242
|
dataChecked: typeof r.dataChecked === "string" ? r.dataChecked : "",
|
|
3211
3243
|
checked: typeof r.checked === "string" ? r.checked : "",
|
|
3212
3244
|
value: typeof r.value === "string" ? r.value : "",
|
|
3245
|
+
ariaExpanded: typeof r.ariaExpanded === "string" ? r.ariaExpanded : "",
|
|
3213
3246
|
};
|
|
3214
3247
|
}
|
|
3215
3248
|
/**
|
|
@@ -3406,6 +3439,76 @@ async function selectionSiblingCommittedValueChanged(target, leafXpath, preSelec
|
|
|
3406
3439
|
return false;
|
|
3407
3440
|
}
|
|
3408
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
|
+
}
|
|
3409
3512
|
/** How long the upload primitive waits for a post-setInputFiles network POST. */
|
|
3410
3513
|
const UPLOAD_NETWORK_TIMEOUT_MS = 5_000;
|
|
3411
3514
|
/** Polling interval while waiting for the upload's network signal. */
|
|
@@ -4556,7 +4659,8 @@ async function submitCaptchaGatedForm(target, responseField = "h-captcha-respons
|
|
|
4556
4659
|
*/
|
|
4557
4660
|
async function applySelectValue(page, target, selIdx, value) {
|
|
4558
4661
|
const setExpr = `((selIdx, value) => {
|
|
4559
|
-
const
|
|
4662
|
+
const isOpenerPairedHidden = ${exports.OPENER_PAIRED_HIDDEN_SELECT_EL_EXPR};
|
|
4663
|
+
const sel = Array.from(document.querySelectorAll("select")).filter((s) => !isOpenerPairedHidden(s))[selIdx];
|
|
4560
4664
|
if (!sel) return { ok: false };
|
|
4561
4665
|
const desc = Object.getOwnPropertyDescriptor(HTMLSelectElement.prototype, "value");
|
|
4562
4666
|
if (desc && desc.set) { desc.set.call(sel, value); } else { sel.value = value; }
|
|
@@ -4571,7 +4675,8 @@ async function applySelectValue(page, target, selIdx, value) {
|
|
|
4571
4675
|
await page.waitForTimeout(SELECT_SETTLE_MS);
|
|
4572
4676
|
const invalidExpr = `((selIdx) => {
|
|
4573
4677
|
const isInvalid = ${INVALID_MARKER_EL_EXPR};
|
|
4574
|
-
const
|
|
4678
|
+
const isOpenerPairedHidden = ${exports.OPENER_PAIRED_HIDDEN_SELECT_EL_EXPR};
|
|
4679
|
+
const sel = Array.from(document.querySelectorAll("select")).filter((s) => !isOpenerPairedHidden(s))[selIdx];
|
|
4575
4680
|
if (!sel) return false;
|
|
4576
4681
|
let node = sel;
|
|
4577
4682
|
for (let depth = 0; depth < 6 && node; depth++) {
|
|
@@ -4609,8 +4714,12 @@ async function trySelectPrimitive(params) {
|
|
|
4609
4714
|
const norm = (s) => (s || "").replace(/\\s+/g, " ").trim().toLowerCase();
|
|
4610
4715
|
const wantOpt = norm(option);
|
|
4611
4716
|
// All native selects, INCLUDING tabindex=-1 (MuiNativeSelect) which the
|
|
4612
|
-
// a11y tree — and therefore Stagehand observe — never surfaces.
|
|
4613
|
-
|
|
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));
|
|
4614
4723
|
if (selects.length === 0) return { selectPresent: false };
|
|
4615
4724
|
// FAST PATH — deterministic option match: exactly one select has an option
|
|
4616
4725
|
// matching the flow's answer text. Unambiguous (the option text itself
|
|
@@ -4800,7 +4909,8 @@ async function tryFillRequiredSelectsPrimitive(params) {
|
|
|
4800
4909
|
// (MUI marks NativeSelect via any of these). Reuses the isUnfilled / selLabelText
|
|
4801
4910
|
// shape from trySelectPrimitive.
|
|
4802
4911
|
const enumerateExpr = `(() => {
|
|
4803
|
-
const
|
|
4912
|
+
const isOpenerPairedHidden = ${exports.OPENER_PAIRED_HIDDEN_SELECT_EL_EXPR};
|
|
4913
|
+
const selects = Array.from(document.querySelectorAll("select")).filter((s) => !isOpenerPairedHidden(s));
|
|
4804
4914
|
if (selects.length === 0) return { candidates: [] };
|
|
4805
4915
|
const selLabelText = (sel) => {
|
|
4806
4916
|
const parts = [];
|
|
@@ -5936,6 +6046,7 @@ async function commitPromptOption(params) {
|
|
|
5936
6046
|
const isRequestedLeaf = norm(chosenOption.text) === wantOpt;
|
|
5937
6047
|
const readbackExpr = `((wIdx, markAttr, valueSel, emptyRxSrc, emptyRxFlags, wantText, isRequestedLeaf) => {
|
|
5938
6048
|
const isInvalid = ${INVALID_MARKER_EL_EXPR};
|
|
6049
|
+
const isPairedHiddenSelect = ${exports.OPENER_PAIRED_HIDDEN_SELECT_EL_EXPR};
|
|
5939
6050
|
const norm = (s) => (s || "").replace(/\\s+/g, " ").trim().toLowerCase();
|
|
5940
6051
|
const buttonValue = ${BUTTON_VALUE_EXPR};
|
|
5941
6052
|
const emptyRx = new RegExp(emptyRxSrc, emptyRxFlags);
|
|
@@ -5958,10 +6069,55 @@ async function commitPromptOption(params) {
|
|
|
5958
6069
|
if (node.getAttribute && isInvalid(node)) { stillInvalid = true; break; }
|
|
5959
6070
|
node = node.parentElement;
|
|
5960
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
|
+
}
|
|
5961
6117
|
// A category click may leave the widget's aria-invalid unset until final
|
|
5962
6118
|
// submit, so !stillInvalid alone only counts as success when this click
|
|
5963
6119
|
// landed on the requested leaf option, not an intermediate category.
|
|
5964
|
-
return { ok: textMatches || (isRequestedLeaf && !stillInvalid), id: w.id || "" };
|
|
6120
|
+
return { ok: textMatches || (isRequestedLeaf && !stillInvalid) || hiddenSelectMatches, id: w.id || "" };
|
|
5965
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)})`;
|
|
5966
6122
|
const readback = (await target.evaluate(readbackExpr).catch(() => ({ ok: false, id: "" })));
|
|
5967
6123
|
if (readback?.ok) {
|
|
@@ -6063,12 +6219,14 @@ async function hasUnfilledRequiredControlForStep(target, instruction) {
|
|
|
6063
6219
|
// (button/aria-haspopup shapes with no native input/select at all) to
|
|
6064
6220
|
// the container-widget union already covered.
|
|
6065
6221
|
const controls = Array.from(document.querySelectorAll(
|
|
6066
|
-
"input,select,textarea,[role=combobox],[role=listbox]
|
|
6222
|
+
"input,select,textarea,[role=combobox],[role=listbox]," + triggerSel
|
|
6067
6223
|
));
|
|
6068
6224
|
for (const el of controls) {
|
|
6069
6225
|
if (!isRequired(el) && !(el.querySelector && el.querySelector("[required],[aria-required=true]"))) {
|
|
6070
6226
|
// container widgets carry required on an inner element; allow those
|
|
6071
|
-
|
|
6227
|
+
const isTriggerWidget =
|
|
6228
|
+
(el.matches && el.matches(triggerSel)) || (el.querySelector && !!el.querySelector(triggerSel));
|
|
6229
|
+
if (!isTriggerWidget) continue;
|
|
6072
6230
|
}
|
|
6073
6231
|
// is it empty/invalid?
|
|
6074
6232
|
const emptyOrInvalid = isEmptyish(el) || (el.querySelector && !!el.querySelector("[aria-invalid=true]"));
|
|
@@ -6329,7 +6487,15 @@ async function verifyDomEffect(target, action, preSelectionState = {}) {
|
|
|
6329
6487
|
// ancestor, and — since some resolved xpaths land on a sibling label rather
|
|
6330
6488
|
// than the combobox itself — the nearest combobox within the same
|
|
6331
6489
|
// field-group container, for a nested input value, falling back to visible
|
|
6332
|
-
// 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).
|
|
6333
6499
|
const exprCustom = `(() => {
|
|
6334
6500
|
const r = document.evaluate(${JSON.stringify(xpath)}, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
|
|
6335
6501
|
const el = r.singleNodeValue;
|
|
@@ -6342,9 +6508,28 @@ async function verifyDomEffect(target, action, preSelectionState = {}) {
|
|
|
6342
6508
|
combo = combo || el;
|
|
6343
6509
|
const nestedInput = combo.querySelector && combo.querySelector('input');
|
|
6344
6510
|
const value = (nestedInput && nestedInput.value) || el.value || "";
|
|
6345
|
-
const
|
|
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() : "";
|
|
6346
6530
|
const ariaText = (el.getAttribute && (el.getAttribute('aria-label') || el.getAttribute('aria-valuetext'))) || "";
|
|
6347
|
-
|
|
6531
|
+
const text = adText || selectedOptText || ownText || ariaText;
|
|
6532
|
+
return { value: String(value).trim(), text: String(text || "").trim() };
|
|
6348
6533
|
})()`;
|
|
6349
6534
|
try {
|
|
6350
6535
|
const customResult = await target.evaluate(exprCustom);
|
|
@@ -6438,7 +6623,17 @@ async function verifyDomEffect(target, action, preSelectionState = {}) {
|
|
|
6438
6623
|
// may live on a hidden sibling `<input>`/`<select>` the click never
|
|
6439
6624
|
// re-renders visibly. Diff that control against its own baseline
|
|
6440
6625
|
// entry instead.
|
|
6441
|
-
|
|
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);
|
|
6442
6637
|
}
|
|
6443
6638
|
const isCheckedNow = await locator.isChecked();
|
|
6444
6639
|
if (!isCheckedNow)
|