@enricai/barnacle 1.12.71 → 1.12.73
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/recon/capture-filters.d.ts +8 -1
- package/dist/recon/capture-filters.d.ts.map +1 -1
- package/dist/recon/capture-filters.js +97 -26
- package/dist/recon/capture-filters.js.map +1 -1
- package/dist/scraper/flow-runner.d.ts +61 -0
- package/dist/scraper/flow-runner.d.ts.map +1 -1
- package/dist/scraper/flow-runner.js +138 -46
- package/dist/scraper/flow-runner.js.map +1 -1
- package/dist/scraper/submit-control.d.ts +23 -1
- package/dist/scraper/submit-control.d.ts.map +1 -1
- package/dist/scraper/submit-control.js +40 -3
- package/dist/scraper/submit-control.js.map +1 -1
- package/dist/scripts/recon-browser.d.ts +10 -2
- package/dist/scripts/recon-browser.d.ts.map +1 -1
- package/dist/scripts/recon-browser.js +93 -27
- package/dist/scripts/recon-browser.js.map +1 -1
- package/dist/scripts/recon-generate.d.ts +148 -6
- package/dist/scripts/recon-generate.d.ts.map +1 -1
- package/dist/scripts/recon-generate.js +557 -80
- package/dist/scripts/recon-generate.js.map +1 -1
- package/package.json +1 -1
|
@@ -19,6 +19,7 @@ exports.mergeResponseHeaders = mergeResponseHeaders;
|
|
|
19
19
|
exports.wireSignalCapture = wireSignalCapture;
|
|
20
20
|
exports.shouldRetryCaptchaRegistry = shouldRetryCaptchaRegistry;
|
|
21
21
|
exports.prepareFailureDumpBody = prepareFailureDumpBody;
|
|
22
|
+
exports.fieldValuesAtFailureExpr = fieldValuesAtFailureExpr;
|
|
22
23
|
exports.snapshotPage = snapshotPage;
|
|
23
24
|
exports.resolveDumpPageIdentity = resolveDumpPageIdentity;
|
|
24
25
|
exports.findRecentBackendError = findRecentBackendError;
|
|
@@ -88,6 +89,7 @@ exports.hasUnfilledRequiredControlForStep = hasUnfilledRequiredControlForStep;
|
|
|
88
89
|
exports.simulateDragDropUpload = simulateDragDropUpload;
|
|
89
90
|
exports.dispatchJqueryChangeEvent = dispatchJqueryChangeEvent;
|
|
90
91
|
exports.verifyDomEffect = verifyDomEffect;
|
|
92
|
+
exports.formValidityProbeExpr = formValidityProbeExpr;
|
|
91
93
|
exports.narrowInvalidFormControl = narrowInvalidFormControl;
|
|
92
94
|
exports.formatStepPrefix = formatStepPrefix;
|
|
93
95
|
exports.tryDeterministicFieldLabelActuation = tryDeterministicFieldLabelActuation;
|
|
@@ -952,6 +954,55 @@ function prepareFailureDumpBody(raw) {
|
|
|
952
954
|
* agree on exactly what counts as a class-token selection marker.
|
|
953
955
|
*/
|
|
954
956
|
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 }; })()`;
|
|
957
|
+
/** Caps mirroring {@link DOM_SNAPSHOT_EXPR}'s truncation so a pathological page can't bloat the failure bundle. */
|
|
958
|
+
const FIELD_VALUES_AT_FAILURE_MAX_CONTROLS = 200;
|
|
959
|
+
const FIELD_VALUES_AT_FAILURE_MAX_VALUE_LENGTH = 500;
|
|
960
|
+
/**
|
|
961
|
+
* Browser-side expression source returning `{label, value}[]` for every
|
|
962
|
+
* visible input/textarea/select, read via the live `.value`/`.checked`
|
|
963
|
+
* property rather than the serialized attribute. Exists because every
|
|
964
|
+
* DOM-direct fill path sets a control's value with `el.value = value` (the
|
|
965
|
+
* JS property), which is never reflected back into the `value=` content
|
|
966
|
+
* attribute `document.body.outerHTML` serializes — so a reparsed HTML
|
|
967
|
+
* snapshot can structurally never prove what a text field currently holds.
|
|
968
|
+
* The accessible-name resolution (`aria-labelledby`, then `aria-label`, then
|
|
969
|
+
* `label[for]`/wrapping `label`, then `name`/`id`/`placeholder`) mirrors
|
|
970
|
+
* `recon-browser.ts`'s `accessibleNameForControl` so the label a step
|
|
971
|
+
* targets and the label this capture keys on agree.
|
|
972
|
+
*/
|
|
973
|
+
function fieldValuesAtFailureExpr() {
|
|
974
|
+
return `(() => {
|
|
975
|
+
const b = document.body;
|
|
976
|
+
if (!b) return [];
|
|
977
|
+
const accessibleName = (el) => {
|
|
978
|
+
const labelledBy = el.getAttribute("aria-labelledby");
|
|
979
|
+
if (labelledBy) {
|
|
980
|
+
const text = labelledBy.split(/\\s+/).map((id) => document.getElementById(id)?.textContent ?? "").join(" ").trim();
|
|
981
|
+
if (text) return text;
|
|
982
|
+
}
|
|
983
|
+
const ariaLabel = el.getAttribute("aria-label");
|
|
984
|
+
if (ariaLabel && ariaLabel.trim()) return ariaLabel;
|
|
985
|
+
const id = el.getAttribute("id");
|
|
986
|
+
if (id) {
|
|
987
|
+
const label = document.querySelector("label[for=\\"" + CSS.escape(id) + "\\"]");
|
|
988
|
+
if (label && label.textContent && label.textContent.trim()) return label.textContent;
|
|
989
|
+
}
|
|
990
|
+
const wrappingLabel = el.closest("label");
|
|
991
|
+
if (wrappingLabel && wrappingLabel.textContent && wrappingLabel.textContent.trim()) return wrappingLabel.textContent;
|
|
992
|
+
return el.getAttribute("name") || id || el.getAttribute("placeholder") || "";
|
|
993
|
+
};
|
|
994
|
+
const controls = Array.from(b.querySelectorAll("input, textarea, select")).filter((el) => el.offsetParent !== null);
|
|
995
|
+
const out = [];
|
|
996
|
+
for (const el of controls) {
|
|
997
|
+
if (out.length >= ${FIELD_VALUES_AT_FAILURE_MAX_CONTROLS}) break;
|
|
998
|
+
const label = accessibleName(el).trim();
|
|
999
|
+
if (!label) continue;
|
|
1000
|
+
const value = (el.type === "checkbox" || el.type === "radio") ? (el.checked ? "1" : "0") : (el.value || "");
|
|
1001
|
+
out.push({ label: label.slice(0, 200), value: String(value).slice(0, ${FIELD_VALUES_AT_FAILURE_MAX_VALUE_LENGTH}) });
|
|
1002
|
+
}
|
|
1003
|
+
return out;
|
|
1004
|
+
})()`;
|
|
1005
|
+
}
|
|
955
1006
|
/**
|
|
956
1007
|
* Captures the pre/post signal triple the submit-verify cascade diffs.
|
|
957
1008
|
* Accepts the optional `page` so a resolved child `FrameTarget` whose
|
|
@@ -1588,9 +1639,10 @@ function isClickViewSwapVerified(params) {
|
|
|
1588
1639
|
// a real network/URL transition, leaving it structurally unverifiable.
|
|
1589
1640
|
// `resolvedElementIsSubmitShaped` closes the same gap from the OTHER
|
|
1590
1641
|
// direction: a step the flow never flagged `submitStep` can still resolve
|
|
1591
|
-
// onto an objectively submit-shaped control
|
|
1592
|
-
//
|
|
1593
|
-
//
|
|
1642
|
+
// onto an objectively submit-shaped control per submit-control.ts's
|
|
1643
|
+
// SUBMIT_SHAPE_EXPR (an explicit `type="submit"` affordance, or a
|
|
1644
|
+
// non-native control that clears the same name/negative-text
|
|
1645
|
+
// corroboration bar) — that control needs the same real-transition proof
|
|
1594
1646
|
// regardless of flow authoring, so a byte-positive form reset can't be
|
|
1595
1647
|
// mistaken for a byte-positive real submit.
|
|
1596
1648
|
if (submitStep || resolvedElementIsSubmitShaped)
|
|
@@ -3281,12 +3333,11 @@ function xpathTailForRetarget(xpath) {
|
|
|
3281
3333
|
* resolve) `FIRST_ORDERED_NODE_TYPE` silently accepts whichever one happens
|
|
3282
3334
|
* to come first in document order — not necessarily the element the primary
|
|
3283
3335
|
* xpath was tracking. Snapshotting every match and preferring one that is
|
|
3284
|
-
* objectively submit-shaped —
|
|
3285
|
-
*
|
|
3286
|
-
*
|
|
3287
|
-
* control
|
|
3288
|
-
*
|
|
3289
|
-
* the plausible candidate instead of an arbitrary one.
|
|
3336
|
+
* objectively submit-shaped — reusing submit-control.ts's exported
|
|
3337
|
+
* {@link SUBMIT_SHAPE_EXPR}, the single site-agnostic predicate every submit-
|
|
3338
|
+
* shape consumer now shares — and not negative-text (reuses submit-
|
|
3339
|
+
* control.ts's own {@link NEGATIVE_TEXT_EXPR} vocabulary rather than a
|
|
3340
|
+
* second copy) recovers the plausible candidate instead of an arbitrary one.
|
|
3290
3341
|
* Falls back to the first document-order match when no candidate clears that
|
|
3291
3342
|
* bar, preserving the previous behavior for every single-match (the common)
|
|
3292
3343
|
* case.
|
|
@@ -3295,15 +3346,7 @@ const XPATH_TAIL_RETARGET_RESOLVE_FN_SRC = `((tail) => {
|
|
|
3295
3346
|
const isNegative = ${submit_control_1.NEGATIVE_TEXT_EXPR};
|
|
3296
3347
|
const accessibleName = (el) =>
|
|
3297
3348
|
(el.getAttribute("aria-label") || el.textContent || "").replace(/\\s+/g, " ").trim().toLowerCase();
|
|
3298
|
-
const
|
|
3299
|
-
const isSubmitShaped = (el) => {
|
|
3300
|
-
if (!el || !el.tagName) return false;
|
|
3301
|
-
const tag = el.tagName.toUpperCase();
|
|
3302
|
-
const type = (el.getAttribute("type") || "").toLowerCase();
|
|
3303
|
-
if (tag === "INPUT" && (type === "submit" || type === "image")) return true;
|
|
3304
|
-
if (tag === "BUTTON" && (type === "submit" || type === "") && el.closest("form")) return true;
|
|
3305
|
-
return isFallbackShaped(el, accessibleName(el), isNegative);
|
|
3306
|
-
};
|
|
3349
|
+
const isSubmitShaped = ${submit_control_1.SUBMIT_SHAPE_EXPR};
|
|
3307
3350
|
const r = document.evaluate("//" + tail, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
|
|
3308
3351
|
const nodes = [];
|
|
3309
3352
|
for (let i = 0; i < r.snapshotLength; i++) nodes.push(r.snapshotItem(i));
|
|
@@ -3564,15 +3607,20 @@ async function resolvedClickTargetStillPresent(target, selector) {
|
|
|
3564
3607
|
* Site-agnostic submit-shape signal read directly from the resolved click
|
|
3565
3608
|
* target — the counterpart to the flow-authored `submitStep` flag. A step
|
|
3566
3609
|
* the flow never flagged can still resolve onto an objectively submit-shaped
|
|
3567
|
-
* control
|
|
3568
|
-
*
|
|
3569
|
-
*
|
|
3570
|
-
*
|
|
3571
|
-
*
|
|
3572
|
-
*
|
|
3573
|
-
*
|
|
3574
|
-
*
|
|
3575
|
-
*
|
|
3610
|
+
* control, per submit-control.ts's exported {@link SUBMIT_SHAPE_EXPR}: an
|
|
3611
|
+
* `<input type="submit">` / `<input type="image">`, a `<button>`/`<input>`
|
|
3612
|
+
* carrying an EXPLICIT `type="submit"` attribute, or — the same
|
|
3613
|
+
* corroboration-requiring path a non-native control must clear — a
|
|
3614
|
+
* tag/role-agnostic control (e.g. a `<div>`/`<a>`) whose accessible name
|
|
3615
|
+
* reads a generic action verb, or is the sole actionable control in its
|
|
3616
|
+
* form. A default-type `<button>` merely owned by a `<form>` no longer
|
|
3617
|
+
* earns unconditional credit from that structural fact alone: the HTML
|
|
3618
|
+
* spec makes a button's default type submit, but structure alone is not
|
|
3619
|
+
* proof of submit intent (e.g. a "Show contact form" reveal toggle a page
|
|
3620
|
+
* happens to nest inside a `<form>`), so it must clear the same
|
|
3621
|
+
* corroboration bar as a non-native control. Reuses submit-control.ts's
|
|
3622
|
+
* own single predicate instead of a hand-duplicated tag/type-only check,
|
|
3623
|
+
* so a legitimately-clicked non-native submit control is not
|
|
3576
3624
|
* mis-classified as "not submit-shaped" here. Returns `false` — never
|
|
3577
3625
|
* manufactures a submit-shape veto — on a non-xpath selector, a miss, or an
|
|
3578
3626
|
* evaluate failure.
|
|
@@ -3596,18 +3644,8 @@ async function resolvedClickTargetIsSubmitShaped(target, selector, xpathTail = n
|
|
|
3596
3644
|
if (!el && ${JSON.stringify(xpathTail)}) {
|
|
3597
3645
|
el = ${XPATH_TAIL_RETARGET_RESOLVE_FN_SRC}(${JSON.stringify(xpathTail)});
|
|
3598
3646
|
}
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
const type = (el.getAttribute("type") || "").toLowerCase();
|
|
3602
|
-
if (tag === "INPUT" && (type === "submit" || type === "image")) return true;
|
|
3603
|
-
if (tag === "BUTTON" && (type === "submit" || type === "") && el.closest("form")) return true;
|
|
3604
|
-
const isNegative = ${submit_control_1.NEGATIVE_TEXT_EXPR};
|
|
3605
|
-
const accessibleName = (node) =>
|
|
3606
|
-
(node.getAttribute("aria-label") || node.textContent || "").replace(/\\s+/g, " ").trim().toLowerCase();
|
|
3607
|
-
const isFallbackShaped = ${submit_control_1.SUBMIT_SHAPE_FALLBACK_EXPR};
|
|
3608
|
-
const name = accessibleName(el);
|
|
3609
|
-
if (isNegative(name)) return false;
|
|
3610
|
-
return isFallbackShaped(el, name, isNegative);
|
|
3647
|
+
const isSubmitShaped = ${submit_control_1.SUBMIT_SHAPE_EXPR};
|
|
3648
|
+
return isSubmitShaped(el);
|
|
3611
3649
|
})()`;
|
|
3612
3650
|
try {
|
|
3613
3651
|
return (await target.evaluate(expr)) === true;
|
|
@@ -4675,9 +4713,12 @@ function parseFieldLabelTarget(step) {
|
|
|
4675
4713
|
* Name"); otherwise a substring match either direction (a `fieldLabel` of
|
|
4676
4714
|
* "Acme Non-Employee ID" against an accessible name of "Non-Employee ID", or
|
|
4677
4715
|
* the reverse) so minor phrasing drift between the flow's field noun and the
|
|
4678
|
-
* control's own label still resolves
|
|
4679
|
-
*
|
|
4680
|
-
*
|
|
4716
|
+
* control's own label still resolves; when multiple candidates qualify via
|
|
4717
|
+
* substring, the one whose length is closest to `fieldLabel`'s (the most
|
|
4718
|
+
* specific match) wins, not whichever happened to come first. Returns
|
|
4719
|
+
* `null` — never a guess — when no candidate's accessible name relates to
|
|
4720
|
+
* `fieldLabel` at all, so the caller can refuse to act rather than
|
|
4721
|
+
* fill/click the wrong control.
|
|
4681
4722
|
*/
|
|
4682
4723
|
function findDeepLocatorCandidateByFieldLabel(candidates, fieldLabel) {
|
|
4683
4724
|
const normalizedLabel = normalizeFieldLabel(fieldLabel);
|
|
@@ -4689,8 +4730,15 @@ function findDeepLocatorCandidateByFieldLabel(candidates, fieldLabel) {
|
|
|
4689
4730
|
const exact = named.find((entry) => entry.text === normalizedLabel);
|
|
4690
4731
|
if (exact)
|
|
4691
4732
|
return exact.candidate;
|
|
4692
|
-
const
|
|
4693
|
-
|
|
4733
|
+
const partials = named.filter((entry) => entry.text.includes(normalizedLabel) || normalizedLabel.includes(entry.text));
|
|
4734
|
+
const mostSpecific = partials.reduce((closest, entry) => {
|
|
4735
|
+
if (!closest)
|
|
4736
|
+
return entry;
|
|
4737
|
+
const entryDistance = Math.abs(entry.text.length - normalizedLabel.length);
|
|
4738
|
+
const closestDistance = Math.abs(closest.text.length - normalizedLabel.length);
|
|
4739
|
+
return entryDistance < closestDistance ? entry : closest;
|
|
4740
|
+
}, null);
|
|
4741
|
+
return mostSpecific?.candidate ?? null;
|
|
4694
4742
|
}
|
|
4695
4743
|
/** Max settle-retry attempts for a primitive's DOM enumerate (see `pollEnumerate`). */
|
|
4696
4744
|
const PRIMITIVE_ENUMERATE_ATTEMPTS = 5;
|
|
@@ -7282,11 +7330,21 @@ async function verifyDomEffect(target, action, preSelectionState = {}) {
|
|
|
7282
7330
|
* - select: pick the first non-empty option.
|
|
7283
7331
|
* Each auto-pick is recorded so the cascade can surface a self-heal
|
|
7284
7332
|
* warning ("you auto-picked X; consider adding an explicit step").
|
|
7333
|
+
* 3. NATIVE-VALIDITY FALLBACK — independent of the class-marker scan:
|
|
7334
|
+
* any `required` control whose native `validity.valueMissing` (or,
|
|
7335
|
+
* absent a `validity` object, an empty `.value`) is true gets flagged
|
|
7336
|
+
* even when no framework has applied an invalid-marker class yet
|
|
7337
|
+
* (e.g. a pristine, untouched required field).
|
|
7285
7338
|
*
|
|
7286
7339
|
* Label resolution checks (in order) the nearest `<label>`, `aria-label`,
|
|
7287
7340
|
* `data-id`, and `name`.
|
|
7341
|
+
*
|
|
7342
|
+
* Exported (mirroring {@link fieldValuesAtFailureExpr}) so the real
|
|
7343
|
+
* native-validity-fallback logic can be exercised against a genuine DOM in
|
|
7344
|
+
* unit tests, rather than only through a mocked `evaluate`.
|
|
7288
7345
|
*/
|
|
7289
|
-
|
|
7346
|
+
function formValidityProbeExpr() {
|
|
7347
|
+
return `(() => {
|
|
7290
7348
|
const INVALID_CLASS_RX = /(${INVALID_MARKER_CLASS_SOURCE})/;
|
|
7291
7349
|
const MARKERS = ["ng-invalid", "mat-form-field-invalid", "is-invalid", "field-invalid", "input-invalid", "Mui-error", "ng-touched", "ng-dirty"];
|
|
7292
7350
|
function fire(el, ev) {
|
|
@@ -7444,9 +7502,35 @@ const FORM_VALIDITY_PROBE_EXPR = `(() => {
|
|
|
7444
7502
|
}
|
|
7445
7503
|
out.push({ label, classSignature, emptyOrUnchecked, autoFilled, _el: el });
|
|
7446
7504
|
}
|
|
7505
|
+
// Native-validity fallback: the class-marker scan above is blind to a
|
|
7506
|
+
// required field that is genuinely empty but PRISTINE -- a framework
|
|
7507
|
+
// that never applies ng-invalid/Mui-error/etc. until after user
|
|
7508
|
+
// interaction, or one that doesn't use any of the listed conventions
|
|
7509
|
+
// at all. This pass is independent of INVALID_CLASS_RX and relies
|
|
7510
|
+
// solely on the native HTML5 constraint-validation API, so it catches
|
|
7511
|
+
// required-and-empty controls no CSS framework has marked invalid yet.
|
|
7512
|
+
const requiredEls = Array.from(document.querySelectorAll("input,select,textarea"));
|
|
7513
|
+
for (const ctrl of requiredEls) {
|
|
7514
|
+
if (!ctrl.required) continue;
|
|
7515
|
+
if (out.some((e) => e._el === ctrl || (e._el && e._el.contains(ctrl)))) continue;
|
|
7516
|
+
const valueMissing = ctrl.validity ? ctrl.validity.valueMissing : !ctrl.value;
|
|
7517
|
+
if (!valueMissing) continue;
|
|
7518
|
+
const byFor = ctrl.id && document.querySelector ? document.querySelector("label[for=\\"" + ctrl.id + "\\"]") : null;
|
|
7519
|
+
let label = byFor && byFor.textContent ? byFor.textContent.trim() : "";
|
|
7520
|
+
let scan = ctrl;
|
|
7521
|
+
for (let i = 0; i < 4 && scan && !label; i++) {
|
|
7522
|
+
const ownLabel = scan.closest ? scan.closest("label") : null;
|
|
7523
|
+
if (ownLabel && ownLabel.textContent) label = ownLabel.textContent.trim();
|
|
7524
|
+
scan = scan.parentElement;
|
|
7525
|
+
}
|
|
7526
|
+
if (!label) label = ctrl.getAttribute("aria-label") || ctrl.getAttribute("data-id") || ctrl.getAttribute("name") || ctrl.getAttribute("id") || "(unlabeled)";
|
|
7527
|
+
label = label.replace(/\\s+/g, " ").slice(0, 80);
|
|
7528
|
+
out.push({ label, classSignature: "", emptyOrUnchecked: true, autoFilled: null, _el: ctrl });
|
|
7529
|
+
}
|
|
7447
7530
|
// Strip the DOM reference before serialization.
|
|
7448
7531
|
return out.slice(0, 12).map((e) => ({ label: e.label, classSignature: e.classSignature, emptyOrUnchecked: e.emptyOrUnchecked, autoFilled: e.autoFilled }));
|
|
7449
7532
|
})()`;
|
|
7533
|
+
}
|
|
7450
7534
|
/**
|
|
7451
7535
|
* Runs ONLY on the cascade's final step when a submitEndpointPattern is
|
|
7452
7536
|
* declared. Surfaces ng-invalid form controls (and whether each is empty)
|
|
@@ -7515,7 +7599,7 @@ function formatStepPrefix(stepIndex, totalSteps) {
|
|
|
7515
7599
|
async function probeFormValidityBeforeSubmit(params) {
|
|
7516
7600
|
const { target, stepIndex, totalSteps, logger } = params;
|
|
7517
7601
|
try {
|
|
7518
|
-
const raw = await target.evaluate(
|
|
7602
|
+
const raw = await target.evaluate(formValidityProbeExpr());
|
|
7519
7603
|
if (!Array.isArray(raw))
|
|
7520
7604
|
return [];
|
|
7521
7605
|
const out = [];
|
|
@@ -8637,6 +8721,9 @@ async function executeStepWithHealing(params) {
|
|
|
8637
8721
|
.evaluate("document.body ? document.body.outerHTML : null")
|
|
8638
8722
|
.catch(() => null);
|
|
8639
8723
|
const bodyOuterHtml = prepareFailureDumpBody(bodyOuterHtmlRaw);
|
|
8724
|
+
const fieldValuesAtFailure = await (frameTarget ?? page)
|
|
8725
|
+
.evaluate(fieldValuesAtFailureExpr())
|
|
8726
|
+
.catch(() => null);
|
|
8640
8727
|
const probeAbsentObservedUnfocused = await (0, stagehand_guard_1.guardedObserve)(stagehand, undefined, { timeout: exports.STEP_WATCHDOG_MS }, captureFn, frameTarget).catch(() => []);
|
|
8641
8728
|
// observe() is blind to a cross-origin OOPIF, so a frame-scoped empty
|
|
8642
8729
|
// result degrades to the deep-locator resolver for dump evidence — this
|
|
@@ -8662,6 +8749,7 @@ async function executeStepWithHealing(params) {
|
|
|
8662
8749
|
bodyOuterHtml,
|
|
8663
8750
|
unfocusedObserve,
|
|
8664
8751
|
targetResolutionDiagnostic,
|
|
8752
|
+
fieldValuesAtFailure,
|
|
8665
8753
|
}) ?? null;
|
|
8666
8754
|
throw new errors_2.StepVerificationError(`${formatStepPrefix(stepIndex, totalSteps)} (${step.slice(0, 60)}) probe found no candidates on page${dumpPath ? `; see ${dumpPath}` : ""}`, "probe-absent");
|
|
8667
8755
|
}
|
|
@@ -10943,6 +11031,9 @@ async function executeStepWithHealing(params) {
|
|
|
10943
11031
|
.evaluate("document.body ? document.body.outerHTML : null")
|
|
10944
11032
|
.catch(() => null);
|
|
10945
11033
|
const bodyOuterHtml = prepareFailureDumpBody(bodyOuterHtmlRaw);
|
|
11034
|
+
const fieldValuesAtFailure = await (frameTarget ?? (0, frame_target_1.mainFrameTarget)(page))
|
|
11035
|
+
.evaluate(fieldValuesAtFailureExpr())
|
|
11036
|
+
.catch(() => null);
|
|
10946
11037
|
const cascadeExhaustObservedUnfocused = await (0, stagehand_guard_1.guardedObserve)(stagehand, undefined, { timeout: exports.STEP_WATCHDOG_MS }, captureFn, frameTarget).catch(() => []);
|
|
10947
11038
|
const unfocusedObserve = cascadeExhaustObservedUnfocused.length === 0 && frameTarget?.frame
|
|
10948
11039
|
? await deepLocatorCandidatesAsActions(page, frameTarget)
|
|
@@ -10962,6 +11053,7 @@ async function executeStepWithHealing(params) {
|
|
|
10962
11053
|
bodyOuterHtml,
|
|
10963
11054
|
unfocusedObserve,
|
|
10964
11055
|
targetResolutionDiagnostic,
|
|
11056
|
+
fieldValuesAtFailure,
|
|
10965
11057
|
}) ?? null;
|
|
10966
11058
|
if (dumpPath !== null) {
|
|
10967
11059
|
logger.error(`${formatStepPrefix(stepIndex, totalSteps)} failed after ${MAX_STEP_ATTEMPTS} attempts; diagnostic bundle: ${dumpPath}`);
|