@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.
@@ -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 (an HTML `type="submit"`
1592
- // affordance, or an unmarked `<button>` owned by a `<form>`, whose default
1593
- // type IS submit) — that control needs the same real-transition proof
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 — native `type="submit"`/`type="image"`/
3285
- * form-owned button, or (reusing submit-control.ts's exported
3286
- * {@link SUBMIT_SHAPE_FALLBACK_EXPR}) a tag/role-agnostic generic-action
3287
- * control — and not negative-text (reuses submit-control.ts's own
3288
- * {@link NEGATIVE_TEXT_EXPR} vocabulary rather than a second copy) recovers
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 isFallbackShaped = ${submit_control_1.SUBMIT_SHAPE_FALLBACK_EXPR};
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: `<input type="submit">` / `<input type="image">`, a `<button>`
3568
- * with no `type` (or `type="submit"`) owned by a `<form>` — per the HTML
3569
- * spec a button's default type IS submit, so an unmarked in-form button
3570
- * commits the form exactly like an explicit `type="submit"` one — or, via
3571
- * {@link SUBMIT_SHAPE_FALLBACK_EXPR}, a tag/role-agnostic control (e.g. a
3572
- * `<div>`/`<a>`) whose accessible name reads a generic action verb, or is the
3573
- * sole actionable control in its form. Reuses submit-control.ts's own
3574
- * widened predicate instead of a narrower, hand-duplicated tag/type-only
3575
- * check, so a legitimately-clicked non-native submit control is not
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
- if (!el || !el.tagName) return false;
3600
- const tag = el.tagName.toUpperCase();
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. Returns `null` — never a guess — when
4679
- * no candidate's accessible name relates to `fieldLabel` at all, so the
4680
- * caller can refuse to act rather than fill/click the wrong control.
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 partial = named.find((entry) => entry.text.includes(normalizedLabel) || normalizedLabel.includes(entry.text));
4693
- return partial?.candidate ?? null;
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
- const FORM_VALIDITY_PROBE_EXPR = `(() => {
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(FORM_VALIDITY_PROBE_EXPR);
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}`);