@enricai/barnacle 1.12.24 → 1.12.26
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/scraper/browser-click-expr.d.ts +88 -0
- package/dist/scraper/browser-click-expr.d.ts.map +1 -1
- package/dist/scraper/browser-click-expr.js +133 -0
- package/dist/scraper/browser-click-expr.js.map +1 -1
- package/dist/scraper/flow-runner.d.ts.map +1 -1
- package/dist/scraper/flow-runner.js +257 -37
- package/dist/scraper/flow-runner.js.map +1 -1
- package/dist/scripts/recon-generate-multicall-fixture.d.ts +22 -0
- package/dist/scripts/recon-generate-multicall-fixture.d.ts.map +1 -1
- package/dist/scripts/recon-generate-multicall-fixture.js +54 -0
- package/dist/scripts/recon-generate-multicall-fixture.js.map +1 -1
- package/dist/scripts/recon-generate.d.ts +9 -0
- package/dist/scripts/recon-generate.d.ts.map +1 -1
- package/dist/scripts/recon-generate.js +77 -11
- package/dist/scripts/recon-generate.js.map +1 -1
- package/package.json +1 -1
|
@@ -670,6 +670,30 @@ const INVALID_MARKER_EL_EXPR = `((el) => {
|
|
|
670
670
|
if (el.getAttribute && el.getAttribute("aria-invalid") === "true") return true;
|
|
671
671
|
return false;
|
|
672
672
|
})`;
|
|
673
|
+
/**
|
|
674
|
+
* Browser-context predicate string: given an element `el` in scope, is it
|
|
675
|
+
* disabled — either directly (native `disabled` property or
|
|
676
|
+
* `aria-disabled="true"`) or via an ancestor within
|
|
677
|
+
* {@link MAX_SELECTION_ANCESTOR_DEPTH} levels? Stagehand can resolve a click
|
|
678
|
+
* to a decorative leaf (a plain `<span>`/text node) nested inside a
|
|
679
|
+
* non-form clickable wrapper (`<div role="button" aria-disabled="true">`) —
|
|
680
|
+
* the leaf itself carries no `disabled` property and no `aria-disabled`
|
|
681
|
+
* attribute of its own, so a leaf-only check misses it even though the
|
|
682
|
+
* click can't have done anything. Climbs the SAME bounded depth as
|
|
683
|
+
* {@link NEARBY_SELECTION_CONTAINER_FN_SRC} rather than an unbounded walk,
|
|
684
|
+
* so a disabled ancestor far up the tree (e.g. a disabled `<fieldset>`
|
|
685
|
+
* wrapping an entire form section) doesn't false-positive-veto an
|
|
686
|
+
* unrelated enabled control nested arbitrarily deep inside it. Interpolate
|
|
687
|
+
* into a `page.evaluate` expr where `el` is bound.
|
|
688
|
+
*/
|
|
689
|
+
const DISABLED_MARKER_EL_EXPR = `((el) => {
|
|
690
|
+
for (let depth = 0; depth < ${browser_click_expr_1.MAX_SELECTION_ANCESTOR_DEPTH} && el; depth++) {
|
|
691
|
+
if (el.disabled === true) return true;
|
|
692
|
+
if (el.getAttribute && el.getAttribute("aria-disabled") === "true") return true;
|
|
693
|
+
el = el.parentElement;
|
|
694
|
+
}
|
|
695
|
+
return false;
|
|
696
|
+
})`;
|
|
673
697
|
/**
|
|
674
698
|
* How many steps from the end of the flow are considered "trailing" for the
|
|
675
699
|
* Tier 1 grace path. A verification failure on an optional step within this
|
|
@@ -709,11 +733,17 @@ function prepareFailureDumpBody(raw) {
|
|
|
709
733
|
return pruned.slice(0, FAILURE_DUMP_MAX_BODY_LENGTH);
|
|
710
734
|
}
|
|
711
735
|
/**
|
|
712
|
-
* Trust boundary:
|
|
713
|
-
*
|
|
714
|
-
*
|
|
736
|
+
* Trust boundary: composed only from compile-time constants
|
|
737
|
+
* ({@link SELECTION_MARKER_CLASS_SELECTOR_SRC},
|
|
738
|
+
* {@link SELECTION_MARKER_CLASS_TOKEN_REGEX_SRC}) — no runtime/request
|
|
739
|
+
* interpolation, so no injection surface. Runs in browser context and returns
|
|
740
|
+
* a typed-narrow shape via Runtime.callFunctionOn. The class-token selector
|
|
741
|
+
* and regex are the SAME source {@link SELECTION_STATE_MAP_EXPR} uses to
|
|
742
|
+
* decide which class-only-marked elements get a strict-verification baseline
|
|
743
|
+
* entry, so the weak page-wide signature and the strict per-element gate
|
|
744
|
+
* agree on exactly what counts as a class-token selection marker.
|
|
715
745
|
*/
|
|
716
|
-
const DOM_SNAPSHOT_EXPR = `(() => { const b = document.body; if (!b) return { html: 0, text: "", values: "", state: "" }; const t = b.innerText || ""; const controls = Array.from(b.querySelectorAll("input, textarea, select")).filter((el) => el.offsetParent !== null); const values = controls.map((el) => { if (el.type === "checkbox" || el.type === "radio") return el.checked ? "1" : "0"; return el.value || ""; }).join("|").slice(0, 2000); const ariaSel = "[aria-pressed],[aria-checked],[aria-selected],[data-state],[data-selected],[data-checked],[role=option],[role=switch],[role=checkbox],[role=menuitemcheckbox]"; const classSel =
|
|
746
|
+
const DOM_SNAPSHOT_EXPR = `(() => { const b = document.body; if (!b) return { html: 0, text: "", values: "", state: "" }; const t = b.innerText || ""; const controls = Array.from(b.querySelectorAll("input, textarea, select")).filter((el) => el.offsetParent !== null); const values = controls.map((el) => { if (el.type === "checkbox" || el.type === "radio") return el.checked ? "1" : "0"; return el.value || ""; }).join("|").slice(0, 2000); const ariaSel = "[aria-pressed],[aria-checked],[aria-selected],[data-state],[data-selected],[data-checked],[role=option],[role=switch],[role=checkbox],[role=menuitemcheckbox]"; const classSel = ${JSON.stringify(browser_click_expr_1.SELECTION_MARKER_CLASS_SELECTOR_SRC)}; const classRx = ${browser_click_expr_1.SELECTION_MARKER_CLASS_TOKEN_REGEX_SRC}; const skip = (el) => el.closest("[role=dialog],[role=tooltip],[aria-live]") !== null; const seen = new Set(); for (const el of b.querySelectorAll(ariaSel + "," + classSel)) { if (el.offsetParent !== null && !skip(el)) seen.add(el); } let idx = 0; const parts = []; for (const el of seen) { const ap = el.getAttribute("aria-pressed") || ""; const ac = el.getAttribute("aria-checked") || ""; const as = el.getAttribute("aria-selected") || ""; const dsRaw = el.getAttribute("data-state") || ""; const ds = (dsRaw === "open" || dsRaw === "closed") ? "" : dsRaw; const dsel = el.hasAttribute("data-selected") ? "1" : ""; const dchk = el.hasAttribute("data-checked") ? "1" : ""; const clsHit = classRx.test(el.getAttribute("class") || "") ? "1" : "0"; const i = idx++; if (!ap && !ac && !as && !ds && !dsel && !dchk && clsHit === "0") continue; parts.push(i + ":" + ap + "," + ac + "," + as + "," + ds + "," + dsel + "," + dchk + "," + clsHit); } const joined = parts.join("|"); let h = 2166136261; for (let k = 0; k < joined.length; k++) { h ^= joined.charCodeAt(k); h = Math.imul(h, 16777619); } const state = (h >>> 0).toString(36) + ":" + parts.length; return { html: (b.outerHTML || "").length, text: t.length + ":" + t.slice(0, 200), values, state }; })()`;
|
|
717
747
|
/**
|
|
718
748
|
* Captures the pre/post signal triple the submit-verify cascade diffs.
|
|
719
749
|
* Accepts the optional `page` so a resolved child `FrameTarget` whose
|
|
@@ -3022,6 +3052,37 @@ function selectionFingerprintObjSrc(elVar, dsVar) {
|
|
|
3022
3052
|
function elementSelectionFingerprintExpr(xpath) {
|
|
3023
3053
|
return `(() => { const r = document.evaluate(${JSON.stringify(xpath)}, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); const el = r.singleNodeValue; if (!el) return null; const ds = el.getAttribute("data-state") || ""; return ${selectionFingerprintObjSrc("el", "ds")}; })()`;
|
|
3024
3054
|
}
|
|
3055
|
+
/**
|
|
3056
|
+
* In-page source for `(el) => Element | null`: the nearest ancestor of `el`,
|
|
3057
|
+
* within {@link MAX_SELECTION_ANCESTOR_DEPTH} levels, whose subtree contains a
|
|
3058
|
+
* selection-marker element (`role="combobox"`/`role="listbox"`, or a
|
|
3059
|
+
* {@link WIDGET_KIT_SELECTION_MARKER_SELECTORS} match) — climbing ancestor
|
|
3060
|
+
* levels rather than a single `closest()` call, because a design-system
|
|
3061
|
+
* combobox's marker (the `role="combobox"` trigger) and its committed-value
|
|
3062
|
+
* control (a hidden `<input>`) are commonly SIBLINGS under a shared wrapper
|
|
3063
|
+
* that itself carries no role/class marker: `closest()` only searches
|
|
3064
|
+
* self-and-ancestors of `el`, so it can never see a marker that lives on a
|
|
3065
|
+
* cousin subtree. At each level, `querySelectorAll` searches the WHOLE
|
|
3066
|
+
* subtree (not just direct children), so the marker may be nested arbitrarily
|
|
3067
|
+
* deep under that ancestor. Returns the FIRST (nearest) qualifying ancestor,
|
|
3068
|
+
* so a same-shaped input/select outside that common ancestor is never
|
|
3069
|
+
* mistaken for the same control's committed-value sibling. Shared by
|
|
3070
|
+
* {@link SELECTION_STATE_MAP_EXPR}'s `isCommittedValueControl` and
|
|
3071
|
+
* {@link selectionSiblingCommittedValueChanged}'s container resolution so the
|
|
3072
|
+
* baseline capture and the read-back agree on exactly what counts as
|
|
3073
|
+
* "nearby" — two independently-drifting copies would silently disagree on
|
|
3074
|
+
* which hidden controls get a baseline entry vs. which get diffed against
|
|
3075
|
+
* one, reintroducing the bug this closes.
|
|
3076
|
+
*/
|
|
3077
|
+
const NEARBY_SELECTION_CONTAINER_FN_SRC = `(el) => {
|
|
3078
|
+
const MARKER_SEL = '[role="combobox"],[role="listbox"],' + ${JSON.stringify(browser_click_expr_1.WIDGET_KIT_SELECTION_MARKER_SELECTORS)};
|
|
3079
|
+
let node = el.parentElement;
|
|
3080
|
+
for (let depth = 0; depth < ${browser_click_expr_1.MAX_SELECTION_ANCESTOR_DEPTH} && node; depth++) {
|
|
3081
|
+
if (node.querySelectorAll && node.querySelectorAll(MARKER_SEL).length > 0) return node;
|
|
3082
|
+
node = node.parentElement;
|
|
3083
|
+
}
|
|
3084
|
+
return null;
|
|
3085
|
+
}`;
|
|
3025
3086
|
/**
|
|
3026
3087
|
* Browser-side expression: build `{ absolutePositionalXpath →
|
|
3027
3088
|
* ElementSelectionFingerprint }` for every VISIBLE interactive element, the
|
|
@@ -3036,22 +3097,40 @@ function elementSelectionFingerprintExpr(xpath) {
|
|
|
3036
3097
|
* rect + `getComputedStyle` idiom as `deep-locator-scan.ts`'s `IS_VISIBLE_EXPR`
|
|
3037
3098
|
* (rather than `offsetParent`), so on-screen `position:fixed` controls — a
|
|
3038
3099
|
* sticky Next/Submit bar, whose `offsetParent` is `null` — still enter the map.
|
|
3100
|
+
* The visibility gate is waived for an `input`/`select` nested inside a nearby
|
|
3101
|
+
* combobox/listbox/group container: a design-system option commits its real
|
|
3102
|
+
* selection to a `display:none` or `aria-hidden` sibling input, not to the
|
|
3103
|
+
* visible trigger label, so excluding it here would make that control
|
|
3104
|
+
* permanently invisible to the read-back — the baseline is where a hidden
|
|
3105
|
+
* committed-value control gets ITS OWN entry (keyed by its own xpath, reusing
|
|
3106
|
+
* the existing `value` field), so a later diff has something to compare
|
|
3107
|
+
* against. The selector also folds in
|
|
3108
|
+
* {@link SELECTION_MARKER_CLASS_SELECTOR_SRC} — the SAME class-token vocabulary
|
|
3109
|
+
* `DOM_SNAPSHOT_EXPR`'s weak diagnostic signature uses — so a custom option
|
|
3110
|
+
* widget that flips a class on ITSELF (`class="option selected"`, no role/
|
|
3111
|
+
* aria-state) gets its own baseline entry: without it,
|
|
3112
|
+
* {@link selectionAncestorChanged}'s ancestor-diff would have nothing to
|
|
3113
|
+
* compare against for a widget that never writes a hidden sibling control.
|
|
3039
3114
|
*/
|
|
3040
3115
|
const SELECTION_STATE_MAP_EXPR = `(() => {
|
|
3041
3116
|
const b = document.body;
|
|
3042
3117
|
if (!b) return {};
|
|
3043
3118
|
const xpathOf = ${XPATH_OF_FN_SRC};
|
|
3119
|
+
const nearbySelectionContainer = ${NEARBY_SELECTION_CONTAINER_FN_SRC};
|
|
3044
3120
|
const visible = (el) => {
|
|
3045
3121
|
const rect = el.getBoundingClientRect();
|
|
3046
3122
|
if (rect.width === 0 && rect.height === 0) return false;
|
|
3047
3123
|
const style = getComputedStyle(el);
|
|
3048
3124
|
return style.display !== "none" && style.visibility !== "hidden";
|
|
3049
3125
|
};
|
|
3050
|
-
const sel = "button,[role=button],a,[tabindex],input,select,textarea,[role=option],[role=tab],[role=switch],[role=checkbox],[role=menuitemcheckbox]";
|
|
3126
|
+
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)};
|
|
3051
3127
|
const skip = (el) => el.closest("[role=dialog],[role=tooltip],[aria-live]") !== null;
|
|
3128
|
+
const isCommittedValueControl = (el) =>
|
|
3129
|
+
(el.tagName === "INPUT" || el.tagName === "SELECT") &&
|
|
3130
|
+
nearbySelectionContainer(el) !== null;
|
|
3052
3131
|
const out = {};
|
|
3053
3132
|
for (const el of b.querySelectorAll(sel)) {
|
|
3054
|
-
if (!visible(el) || skip(el)) continue;
|
|
3133
|
+
if ((!visible(el) && !isCommittedValueControl(el)) || skip(el)) continue;
|
|
3055
3134
|
const ds = el.getAttribute("data-state") || "";
|
|
3056
3135
|
out[xpathOf(el)] = ${selectionFingerprintObjSrc("el", "ds")};
|
|
3057
3136
|
}
|
|
@@ -3129,30 +3208,6 @@ async function readElementSelectionFingerprint(target, selector) {
|
|
|
3129
3208
|
return null;
|
|
3130
3209
|
}
|
|
3131
3210
|
}
|
|
3132
|
-
/**
|
|
3133
|
-
* How far up from the clicked leaf {@link selectionAncestorChanged} walks
|
|
3134
|
-
* looking for the option/toggle that carries the selection. Design-system
|
|
3135
|
-
* options nest their label 1-2 levels deep (a `<span title>` inside a
|
|
3136
|
-
* `role="option"`, plus the odd icon/wrapper); 6 matches the vacuous-click
|
|
3137
|
-
* ancestor guard in {@link verifyDomEffect}'s click branch and covers that
|
|
3138
|
-
* nesting without over-reaching into an outer listbox/group.
|
|
3139
|
-
*/
|
|
3140
|
-
const MAX_SELECTION_ANCESTOR_DEPTH = 6;
|
|
3141
|
-
/**
|
|
3142
|
-
* Cross-vendor selector union for a selection-state widget that carries NO
|
|
3143
|
-
* standard selection `role` or `aria-*`/`data-state` marker — a component-kit
|
|
3144
|
-
* container whose selected-ness lives only in the library's own private
|
|
3145
|
-
* attribute. Same multi-vendor-union discipline as {@link INVALID_MARKER_CLASS_SOURCE}
|
|
3146
|
-
* and the `PROMPT_*_SELECTORS` unions: standards are checked FIRST (see
|
|
3147
|
-
* `hasMarker` in {@link selectionAncestorChanged}); this union is the fallback
|
|
3148
|
-
* for widgets that under-annotate ARIA, and no member is a per-site branch —
|
|
3149
|
-
* each is one component library's signature. Grows by a one-line edit.
|
|
3150
|
-
*
|
|
3151
|
-
* Members: `data-baseweb` (Uber Base Web — verified in a real capture to mark
|
|
3152
|
-
* 150 selection elements that expose no role/aria-state, so dropping it loses
|
|
3153
|
-
* real coverage). Add other under-annotating kits here as they surface.
|
|
3154
|
-
*/
|
|
3155
|
-
const WIDGET_KIT_SELECTION_MARKER_SELECTORS = ["[data-baseweb]"].join(",");
|
|
3156
3211
|
/**
|
|
3157
3212
|
* Element-scoped selection read-back for the case the clicked node's OWN
|
|
3158
3213
|
* fingerprint can't credit: a design-system option that wraps its label in a
|
|
@@ -3185,13 +3240,26 @@ async function selectionAncestorChanged(target, leafXpath, preSelectionState) {
|
|
|
3185
3240
|
const xpathOf = ${XPATH_OF_FN_SRC};
|
|
3186
3241
|
const fp = (el) => { const ds = el.getAttribute("data-state") || ""; return ${selectionFingerprintObjSrc("el", "ds")}; };
|
|
3187
3242
|
const SELECTION_ROLES = new Set(["option", "tab", "switch", "radio", "checkbox", "menuitemcheckbox"]);
|
|
3188
|
-
const KIT_MARKER_SEL = ${JSON.stringify(WIDGET_KIT_SELECTION_MARKER_SELECTORS)};
|
|
3243
|
+
const KIT_MARKER_SEL = ${JSON.stringify(browser_click_expr_1.WIDGET_KIT_SELECTION_MARKER_SELECTORS)};
|
|
3244
|
+
const CLASS_TOKEN_RX = ${browser_click_expr_1.SELECTION_MARKER_CLASS_TOKEN_REGEX_SRC};
|
|
3189
3245
|
const hasMarker = (el, f) => {
|
|
3190
3246
|
// Standards first: fingerprint fields / aria-states, then a selection role.
|
|
3191
3247
|
if (f.kind || f.ariaPressed || f.ariaChecked || f.ariaSelected || f.dataState || f.dataSelected || f.dataChecked || f.checked) return true;
|
|
3192
3248
|
if (SELECTION_ROLES.has((el.getAttribute("role") || "").toLowerCase())) return true;
|
|
3193
3249
|
// Fallback: a component-kit selection widget that exposes no standard marker.
|
|
3194
3250
|
if (el.matches(KIT_MARKER_SEL)) return true;
|
|
3251
|
+
// Fallback: a widget authored with no role/aria-state, marked purely by a class token.
|
|
3252
|
+
if (CLASS_TOKEN_RX.test(el.getAttribute("class") || "")) return true;
|
|
3253
|
+
// Fallback: a broken click handler never adds the token to THIS node, but a
|
|
3254
|
+
// sibling still carrying it (the untouched prior selection) proves the
|
|
3255
|
+
// group uses the class-token convention — so this node is a member of it.
|
|
3256
|
+
if (el.parentElement && el.parentElement.children) {
|
|
3257
|
+
const siblings = el.parentElement.children;
|
|
3258
|
+
for (let i = 0; i < siblings.length; i++) {
|
|
3259
|
+
const sib = siblings[i];
|
|
3260
|
+
if (sib !== el && CLASS_TOKEN_RX.test(sib.getAttribute("class") || "")) return true;
|
|
3261
|
+
}
|
|
3262
|
+
}
|
|
3195
3263
|
return false;
|
|
3196
3264
|
};
|
|
3197
3265
|
const changed = (a, b) =>
|
|
@@ -3202,7 +3270,7 @@ async function selectionAncestorChanged(target, leafXpath, preSelectionState) {
|
|
|
3202
3270
|
const r = document.evaluate(LEAF, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
|
|
3203
3271
|
let node = r.singleNodeValue;
|
|
3204
3272
|
if (!node) return false;
|
|
3205
|
-
for (let depth = 0; depth < ${MAX_SELECTION_ANCESTOR_DEPTH} && node; depth++) {
|
|
3273
|
+
for (let depth = 0; depth < ${browser_click_expr_1.MAX_SELECTION_ANCESTOR_DEPTH} && node; depth++) {
|
|
3206
3274
|
if (node.getAttribute) {
|
|
3207
3275
|
const now = fp(node);
|
|
3208
3276
|
if (hasMarker(node, now)) {
|
|
@@ -3222,6 +3290,100 @@ async function selectionAncestorChanged(target, leafXpath, preSelectionState) {
|
|
|
3222
3290
|
return false;
|
|
3223
3291
|
}
|
|
3224
3292
|
}
|
|
3293
|
+
/**
|
|
3294
|
+
* Structural marker probe for the n+16 fallback's weak-signal gate — reuses
|
|
3295
|
+
* the SAME `hasMarker` predicate {@link selectionAncestorChanged} diffs
|
|
3296
|
+
* against, but only asks presence, not before/after equality. A click that
|
|
3297
|
+
* resolves onto (or under) a `role=option`/aria-selection/component-kit
|
|
3298
|
+
* marker element must clear the strict {@link selectionAncestorChanged} /
|
|
3299
|
+
* {@link selectionSiblingCommittedValueChanged} signal to be credited —
|
|
3300
|
+
* html-byte-delta/text-change/form-value alone (a label re-render with no
|
|
3301
|
+
* committed-control change) is exactly the reported defect
|
|
3302
|
+
* (textChanged=true, selectionStateChanged=false, verified=true) this
|
|
3303
|
+
* closes off. Returns `false` on any miss / malformed result / evaluate
|
|
3304
|
+
* throw (defer to the strict signal already gating `retryVerified`).
|
|
3305
|
+
*/
|
|
3306
|
+
async function clickTargetHasSelectionMarker(target, leafXpath) {
|
|
3307
|
+
const expr = `(() => {
|
|
3308
|
+
const LEAF = ${JSON.stringify(leafXpath)};
|
|
3309
|
+
const fp = (el) => { const ds = el.getAttribute("data-state") || ""; return ${selectionFingerprintObjSrc("el", "ds")}; };
|
|
3310
|
+
const SELECTION_ROLES = new Set(["option", "tab", "switch", "radio", "checkbox", "menuitemcheckbox"]);
|
|
3311
|
+
const KIT_MARKER_SEL = ${JSON.stringify(browser_click_expr_1.WIDGET_KIT_SELECTION_MARKER_SELECTORS)};
|
|
3312
|
+
const CLASS_TOKEN_RX = ${browser_click_expr_1.SELECTION_MARKER_CLASS_TOKEN_REGEX_SRC};
|
|
3313
|
+
const hasMarker = (el, f) => {
|
|
3314
|
+
if (f.kind || f.ariaPressed || f.ariaChecked || f.ariaSelected || f.dataState || f.dataSelected || f.dataChecked || f.checked) return true;
|
|
3315
|
+
if (SELECTION_ROLES.has((el.getAttribute("role") || "").toLowerCase())) return true;
|
|
3316
|
+
if (el.matches(KIT_MARKER_SEL)) return true;
|
|
3317
|
+
if (CLASS_TOKEN_RX.test(el.getAttribute("class") || "")) return true;
|
|
3318
|
+
if (el.parentElement && el.parentElement.children) {
|
|
3319
|
+
const siblings = el.parentElement.children;
|
|
3320
|
+
for (let i = 0; i < siblings.length; i++) {
|
|
3321
|
+
const sib = siblings[i];
|
|
3322
|
+
if (sib !== el && CLASS_TOKEN_RX.test(sib.getAttribute("class") || "")) return true;
|
|
3323
|
+
}
|
|
3324
|
+
}
|
|
3325
|
+
return false;
|
|
3326
|
+
};
|
|
3327
|
+
const r = document.evaluate(LEAF, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
|
|
3328
|
+
let node = r.singleNodeValue;
|
|
3329
|
+
if (!node) return false;
|
|
3330
|
+
for (let depth = 0; depth < ${browser_click_expr_1.MAX_SELECTION_ANCESTOR_DEPTH} && node; depth++) {
|
|
3331
|
+
if (node.getAttribute && hasMarker(node, fp(node))) return true;
|
|
3332
|
+
node = node.parentElement;
|
|
3333
|
+
}
|
|
3334
|
+
return false;
|
|
3335
|
+
})()`;
|
|
3336
|
+
try {
|
|
3337
|
+
return (await target.evaluate(expr)) === true;
|
|
3338
|
+
}
|
|
3339
|
+
catch {
|
|
3340
|
+
return false;
|
|
3341
|
+
}
|
|
3342
|
+
}
|
|
3343
|
+
/**
|
|
3344
|
+
* Sibling read-back for the case NEITHER the clicked leaf NOR any of its
|
|
3345
|
+
* ancestors ({@link selectionAncestorChanged}) ever change: a design-system
|
|
3346
|
+
* combobox where the click re-renders only the visible trigger label, and the
|
|
3347
|
+
* REAL committed selection lives on a hidden associated `<input>`/`<select>`
|
|
3348
|
+
* that is a sibling/cousin of the leaf, not an ancestor — so an UP-only walk
|
|
3349
|
+
* can never reach it. Finds the nearest combobox/listbox/group container
|
|
3350
|
+
* around the leaf (the same idiom the `selectOption` verifier uses at the
|
|
3351
|
+
* nested-input search), diffs every `input`/`select` inside it against the
|
|
3352
|
+
* baseline entry {@link SELECTION_STATE_MAP_EXPR} captured for that control's
|
|
3353
|
+
* OWN xpath (visibility-gate-waived there for exactly this control class).
|
|
3354
|
+
* Returns `false` on any miss / malformed result / evaluate throw (defer to
|
|
3355
|
+
* the ancestor and network/URL signals).
|
|
3356
|
+
*/
|
|
3357
|
+
async function selectionSiblingCommittedValueChanged(target, leafXpath, preSelectionState) {
|
|
3358
|
+
const expr = `(() => {
|
|
3359
|
+
const LEAF = ${JSON.stringify(leafXpath)};
|
|
3360
|
+
const BASE = ${JSON.stringify(preSelectionState)};
|
|
3361
|
+
const xpathOf = ${XPATH_OF_FN_SRC};
|
|
3362
|
+
const nearbySelectionContainer = ${NEARBY_SELECTION_CONTAINER_FN_SRC};
|
|
3363
|
+
const r = document.evaluate(LEAF, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
|
|
3364
|
+
const leaf = r.singleNodeValue;
|
|
3365
|
+
if (!leaf) return false;
|
|
3366
|
+
// The hidden committed-value control's marker (the role=combobox trigger)
|
|
3367
|
+
// is commonly a SIBLING of the leaf and the control, not an ancestor of
|
|
3368
|
+
// either — climb bounded ancestor levels for the nearest subtree that
|
|
3369
|
+
// contains a marker, rather than a single self-and-ancestors closest().
|
|
3370
|
+
const container = nearbySelectionContainer(leaf) || leaf.parentElement;
|
|
3371
|
+
if (!container || !container.querySelectorAll) return false;
|
|
3372
|
+
for (const control of container.querySelectorAll("input,select")) {
|
|
3373
|
+
const pre = BASE[xpathOf(control)];
|
|
3374
|
+
if (!pre) continue;
|
|
3375
|
+
const now = typeof control.value === "string" ? control.value.slice(0, 200) : "";
|
|
3376
|
+
if (pre.value !== now) return true;
|
|
3377
|
+
}
|
|
3378
|
+
return false;
|
|
3379
|
+
})()`;
|
|
3380
|
+
try {
|
|
3381
|
+
return (await target.evaluate(expr)) === true;
|
|
3382
|
+
}
|
|
3383
|
+
catch {
|
|
3384
|
+
return false;
|
|
3385
|
+
}
|
|
3386
|
+
}
|
|
3225
3387
|
/** How long the upload primitive waits for a post-setInputFiles network POST. */
|
|
3226
3388
|
const UPLOAD_NETWORK_TIMEOUT_MS = 5_000;
|
|
3227
3389
|
/** Polling interval while waiting for the upload's network signal. */
|
|
@@ -5960,6 +6122,19 @@ async function verifyDomEffect(target, action, preSelectionState = {}) {
|
|
|
5960
6122
|
const xpath = xpathBody(selector);
|
|
5961
6123
|
if (!xpath)
|
|
5962
6124
|
return false;
|
|
6125
|
+
// A click that resolved to a disabled (or aria-disabled) target can't
|
|
6126
|
+
// have done anything — veto before trusting any other signal below.
|
|
6127
|
+
const targetDisabledExpr = `(() => {
|
|
6128
|
+
const isDisabled = ${DISABLED_MARKER_EL_EXPR};
|
|
6129
|
+
const r = document.evaluate(${JSON.stringify(xpath)}, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
|
|
6130
|
+
const el = r.singleNodeValue;
|
|
6131
|
+
return el ? isDisabled(el) : false;
|
|
6132
|
+
})()`;
|
|
6133
|
+
const targetDisabled = await target
|
|
6134
|
+
.evaluate(targetDisabledExpr)
|
|
6135
|
+
.catch(() => false);
|
|
6136
|
+
if (targetDisabled)
|
|
6137
|
+
return false;
|
|
5963
6138
|
let inputType = null;
|
|
5964
6139
|
try {
|
|
5965
6140
|
// Trust boundary: xpath comes from Stagehand's own resolved selector
|
|
@@ -5996,7 +6171,14 @@ async function verifyDomEffect(target, action, preSelectionState = {}) {
|
|
|
5996
6171
|
// selection on the ancestor `role="option"`, not the clicked leaf —
|
|
5997
6172
|
// so walk up to the nearest baseline-present selection ancestor and
|
|
5998
6173
|
// diff THAT. No eligible ancestor → false (defer to network/URL).
|
|
5999
|
-
|
|
6174
|
+
if (await selectionAncestorChanged(target, xpath, preSelectionState)) {
|
|
6175
|
+
return true;
|
|
6176
|
+
}
|
|
6177
|
+
// Neither the leaf nor any ancestor moved: the widget's real commit
|
|
6178
|
+
// may live on a hidden sibling `<input>`/`<select>` the click never
|
|
6179
|
+
// re-renders visibly. Diff that control against its own baseline
|
|
6180
|
+
// entry instead.
|
|
6181
|
+
return await selectionSiblingCommittedValueChanged(target, xpath, preSelectionState);
|
|
6000
6182
|
}
|
|
6001
6183
|
const isCheckedNow = await locator.isChecked();
|
|
6002
6184
|
if (!isCheckedNow)
|
|
@@ -8291,7 +8473,7 @@ async function executeStepWithHealing(params) {
|
|
|
8291
8473
|
// even though the leaf element is still live; re-anchor on the
|
|
8292
8474
|
// leaf's own last two steps before giving up.
|
|
8293
8475
|
const xpathTail = xpathTailForRetarget(xpath);
|
|
8294
|
-
const clickExpr = `(() => { const r = document.evaluate(${JSON.stringify(xpath)}, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); let el = r.singleNodeValue; if (!el && ${JSON.stringify(xpathTail)}) { const r2 = document.evaluate("//" + ${JSON.stringify(xpathTail)}, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); el = r2.singleNodeValue; } if (!el || typeof el.click !== "function") return { fired: false }; if (el.tagName === "LABEL") { const wrapped = el.querySelector("input[type=checkbox], input[type=radio]"); if (wrapped) el = wrapped; } if (el.type === "checkbox" || el.type === "radio") { el.checked = true; el.dispatchEvent(new Event("click", { bubbles: true })); el.dispatchEvent(new Event("change", { bubbles: true })); return { fired: true, kind: "checkbox", checked: el.checked }; } ${(0, browser_click_expr_1.clickActivationExpr)("el")} return { fired: true, kind: "click" }; })()`;
|
|
8476
|
+
const clickExpr = `(() => { const r = document.evaluate(${JSON.stringify(xpath)}, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); let el = r.singleNodeValue; if (!el && ${JSON.stringify(xpathTail)}) { const r2 = document.evaluate("//" + ${JSON.stringify(xpathTail)}, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); el = r2.singleNodeValue; } if (!el || typeof el.click !== "function") return { fired: false }; if (el.tagName === "LABEL") { const wrapped = el.querySelector("input[type=checkbox], input[type=radio]"); if (wrapped) el = wrapped; } if (el.type === "checkbox" || el.type === "radio") { el.checked = true; el.dispatchEvent(new Event("click", { bubbles: true })); el.dispatchEvent(new Event("change", { bubbles: true })); return { fired: true, kind: "checkbox", checked: el.checked }; } let __n16SmMatched = false; ${(0, browser_click_expr_1.retargetToSelectionMarkerExpr)("el", "__n16SmMatched")} ${(0, browser_click_expr_1.clickActivationExpr)("el")} if (__n16SmMatched) { el.dispatchEvent(new Event("change", { bubbles: true })); } return { fired: true, kind: "click" }; })()`;
|
|
8295
8477
|
const n16FallbackTarget = frameTarget ?? (0, frame_target_1.mainFrameTarget)(page);
|
|
8296
8478
|
const probeResult = (await n16FallbackTarget.evaluate(clickExpr));
|
|
8297
8479
|
const fired = probeResult.fired;
|
|
@@ -8362,7 +8544,14 @@ async function executeStepWithHealing(params) {
|
|
|
8362
8544
|
// to the nearest baseline-present selection ancestor and diff THAT,
|
|
8363
8545
|
// the same fallback `verifyDomEffect`'s primary read-back uses. No
|
|
8364
8546
|
// eligible ancestor → false (defer to the other retry signals).
|
|
8365
|
-
|
|
8547
|
+
if (await selectionAncestorChanged(frameTarget ?? (0, frame_target_1.mainFrameTarget)(page), xpath, pre.selectionStateByXpath)) {
|
|
8548
|
+
return true;
|
|
8549
|
+
}
|
|
8550
|
+
// Neither the leaf nor any ancestor moved: the widget's real
|
|
8551
|
+
// commit may live on a hidden sibling `<input>`/`<select>` the
|
|
8552
|
+
// click never re-renders visibly. Diff that control against its
|
|
8553
|
+
// own baseline entry instead.
|
|
8554
|
+
return await selectionSiblingCommittedValueChanged(frameTarget ?? (0, frame_target_1.mainFrameTarget)(page), xpath, pre.selectionStateByXpath);
|
|
8366
8555
|
})());
|
|
8367
8556
|
// RC2: an advance/`kind=click` "Next" that only grew the DOM
|
|
8368
8557
|
// (validation errors rendered) with NO network/URL change is a
|
|
@@ -8375,6 +8564,27 @@ async function executeStepWithHealing(params) {
|
|
|
8375
8564
|
// verifying so the cascade routes to the fill-invalid-fields replan.
|
|
8376
8565
|
// Confirmed no-op signature on the wizard ATS's COMPENSATION page (network=false
|
|
8377
8566
|
// url=false htmlDelta>0 textChanged) mis-scored verified=true(dom).
|
|
8567
|
+
// A click that resolved to a disabled (or aria-disabled) target
|
|
8568
|
+
// can't have done anything — same veto as verifyDomEffect's click
|
|
8569
|
+
// branch, applied here so the n+16 fallback can't ride past it on
|
|
8570
|
+
// a weak htmlDelta/textChanged signal.
|
|
8571
|
+
const clickBlockedByDisabled = probeResult.kind === "click" &&
|
|
8572
|
+
xpath !== null &&
|
|
8573
|
+
(await (async () => {
|
|
8574
|
+
const disabledExpr = `(() => {
|
|
8575
|
+
const isDisabled = ${DISABLED_MARKER_EL_EXPR};
|
|
8576
|
+
const r = document.evaluate(${JSON.stringify(xpath)}, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
|
|
8577
|
+
let el = r.singleNodeValue;
|
|
8578
|
+
if (!el && ${JSON.stringify(xpathTail)}) {
|
|
8579
|
+
const r2 = document.evaluate("//" + ${JSON.stringify(xpathTail)}, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
|
|
8580
|
+
el = r2.singleNodeValue;
|
|
8581
|
+
}
|
|
8582
|
+
return el ? isDisabled(el) : false;
|
|
8583
|
+
})()`;
|
|
8584
|
+
return await (frameTarget ?? (0, frame_target_1.mainFrameTarget)(page))
|
|
8585
|
+
.evaluate(disabledExpr)
|
|
8586
|
+
.catch(() => false);
|
|
8587
|
+
})());
|
|
8378
8588
|
const clickWasDomOnly = probeResult.kind === "click" && !retryNetworkFired && !retryUrlChanged;
|
|
8379
8589
|
const clickBlockedByInvalid = clickWasDomOnly &&
|
|
8380
8590
|
(await countNgInvalidContainers(frameTarget ?? (0, frame_target_1.mainFrameTarget)(page)).catch(() => 0)) >
|
|
@@ -8429,9 +8639,19 @@ async function executeStepWithHealing(params) {
|
|
|
8429
8639
|
// earlier fill included), so it says nothing about whether THIS
|
|
8430
8640
|
// control committed — only the element's own `checkboxStateVerified`
|
|
8431
8641
|
// (or a real network/url/selection-state signal) may verify it.
|
|
8642
|
+
// A click that resolved onto (or under) a selection-marker element
|
|
8643
|
+
// (role=option, aria-selected/checked/pressed, a component-kit
|
|
8644
|
+
// marker) must clear the strict retrySelectionStateChanged signal —
|
|
8645
|
+
// the weak html/text/form-value OR-branch alone can never credit
|
|
8646
|
+
// it, closing the reported textChanged=true/selectionStateChanged=
|
|
8647
|
+
// false/verified=true defect.
|
|
8648
|
+
const clickTargetIsSelectionMarker = xpath !== null &&
|
|
8649
|
+
(await clickTargetHasSelectionMarker(frameTarget ?? (0, frame_target_1.mainFrameTarget)(page), xpath));
|
|
8432
8650
|
const weakDomSignalsAllowed = ((!isFinalStep && !submitStep) || requireSubmitEndpoint) &&
|
|
8433
|
-
!isCheckboxOrRadioIntentStep(step)
|
|
8434
|
-
|
|
8651
|
+
!isCheckboxOrRadioIntentStep(step) &&
|
|
8652
|
+
!clickTargetIsSelectionMarker;
|
|
8653
|
+
let retryVerified = !clickBlockedByDisabled &&
|
|
8654
|
+
!clickBlockedByInvalid &&
|
|
8435
8655
|
!fallbackDomOnlyAdvance &&
|
|
8436
8656
|
(retryNetworkFired ||
|
|
8437
8657
|
retryUrlChanged ||
|