@enricai/barnacle 1.12.81 → 1.13.0
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/lib/ats-field-vocabulary.d.ts.map +1 -1
- package/dist/lib/ats-field-vocabulary.js +86 -3
- package/dist/lib/ats-field-vocabulary.js.map +1 -1
- package/dist/lib/llm/judges/verify-step-advanced.d.ts +1 -1
- package/dist/lib/llm/judges/verify-step-advanced.d.ts.map +1 -1
- package/dist/lib/llm/judges/verify-step-advanced.js +5 -3
- package/dist/lib/llm/judges/verify-step-advanced.js.map +1 -1
- package/dist/lib/llm/judges/verify-submit.d.ts +2 -2
- package/dist/lib/llm/judges/verify-submit.d.ts.map +1 -1
- package/dist/lib/llm/judges/verify-submit.js +17 -5
- package/dist/lib/llm/judges/verify-submit.js.map +1 -1
- package/dist/lib/random.d.ts +7 -3
- package/dist/lib/random.d.ts.map +1 -1
- package/dist/lib/random.js +27 -4
- package/dist/lib/random.js.map +1 -1
- package/dist/plugins/config-plugin.d.ts +1 -0
- package/dist/plugins/config-plugin.d.ts.map +1 -1
- package/dist/plugins/config-plugin.js +7 -1
- package/dist/plugins/config-plugin.js.map +1 -1
- package/dist/plugins/loader.d.ts +2 -2
- package/dist/plugins/loader.d.ts.map +1 -1
- package/dist/plugins/loader.js +6 -3
- package/dist/plugins/loader.js.map +1 -1
- package/dist/scraper/click-settle.d.ts +17 -2
- package/dist/scraper/click-settle.d.ts.map +1 -1
- package/dist/scraper/click-settle.js +36 -30
- package/dist/scraper/click-settle.js.map +1 -1
- package/dist/scraper/deep-locator-actuate.d.ts.map +1 -1
- package/dist/scraper/deep-locator-actuate.js +17 -1
- package/dist/scraper/deep-locator-actuate.js.map +1 -1
- package/dist/scraper/deep-locator-fake.js +1 -1
- package/dist/scraper/deep-locator-fake.js.map +1 -1
- package/dist/scraper/errors.d.ts +23 -2
- package/dist/scraper/errors.d.ts.map +1 -1
- package/dist/scraper/errors.js +32 -4
- package/dist/scraper/errors.js.map +1 -1
- package/dist/scraper/flow-runner.d.ts +176 -36
- package/dist/scraper/flow-runner.d.ts.map +1 -1
- package/dist/scraper/flow-runner.js +778 -326
- package/dist/scraper/flow-runner.js.map +1 -1
- package/dist/scraper/http-client.d.ts +7 -2
- package/dist/scraper/http-client.d.ts.map +1 -1
- package/dist/scraper/http-client.js +132 -19
- package/dist/scraper/http-client.js.map +1 -1
- package/dist/scraper/metrics.d.ts +2 -2
- package/dist/scraper/metrics.js +2 -2
- package/dist/scraper/phantom-click.d.ts +51 -1
- package/dist/scraper/phantom-click.d.ts.map +1 -1
- package/dist/scraper/phantom-click.js +170 -19
- package/dist/scraper/phantom-click.js.map +1 -1
- package/dist/scraper/runtime-capture-store.d.ts +38 -0
- package/dist/scraper/runtime-capture-store.d.ts.map +1 -0
- package/dist/scraper/runtime-capture-store.js +103 -0
- package/dist/scraper/runtime-capture-store.js.map +1 -0
- package/dist/scraper/segmented-date-spinbutton.d.ts +13 -0
- package/dist/scraper/segmented-date-spinbutton.d.ts.map +1 -0
- package/dist/scraper/segmented-date-spinbutton.js +71 -0
- package/dist/scraper/segmented-date-spinbutton.js.map +1 -0
- package/dist/scripts/recon-browser.d.ts +0 -8
- package/dist/scripts/recon-browser.d.ts.map +1 -1
- package/dist/scripts/recon-browser.js +102 -50
- package/dist/scripts/recon-browser.js.map +1 -1
- package/dist/scripts/recon-generate.d.ts +11 -0
- package/dist/scripts/recon-generate.d.ts.map +1 -1
- package/dist/scripts/recon-generate.js +180 -53
- package/dist/scripts/recon-generate.js.map +1 -1
- package/dist/site-plugin.d.ts +7 -4
- package/dist/site-plugin.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -34,6 +34,7 @@ exports.classifyStepSubmitIntent = classifyStepSubmitIntent;
|
|
|
34
34
|
exports.isStepDeclaredNonSubmit = isStepDeclaredNonSubmit;
|
|
35
35
|
exports.isStepSubmitShaped = isStepSubmitShaped;
|
|
36
36
|
exports.isStepSettleWaitShaped = isStepSettleWaitShaped;
|
|
37
|
+
exports.isGenuineFinalSubmit = isGenuineFinalSubmit;
|
|
37
38
|
exports.isMidFlowSubmitShapedStep = isMidFlowSubmitShapedStep;
|
|
38
39
|
exports.isCheckboxOrRadioIntentStep = isCheckboxOrRadioIntentStep;
|
|
39
40
|
exports.shouldCaptureSelectionState = shouldCaptureSelectionState;
|
|
@@ -47,6 +48,7 @@ exports.windowHasAdvanceTransition = windowHasAdvanceTransition;
|
|
|
47
48
|
exports.shouldVetoFallbackAdvance = shouldVetoFallbackAdvance;
|
|
48
49
|
exports.isDomOnlyAdvanceVerified = isDomOnlyAdvanceVerified;
|
|
49
50
|
exports.isNullVerdictAdvanceCredited = isNullVerdictAdvanceCredited;
|
|
51
|
+
exports.isMidFlowAdvanceCredited = isMidFlowAdvanceCredited;
|
|
50
52
|
exports.isClickViewSwapVerified = isClickViewSwapVerified;
|
|
51
53
|
exports.selectionCountFromSignature = selectionCountFromSignature;
|
|
52
54
|
exports.shouldReadbackFillOnActSuccess = shouldReadbackFillOnActSuccess;
|
|
@@ -74,10 +76,12 @@ exports.extractGaEventEvidence = extractGaEventEvidence;
|
|
|
74
76
|
exports.normalizeDateValue = normalizeDateValue;
|
|
75
77
|
exports.fillHtml5DateTimeInput = fillHtml5DateTimeInput;
|
|
76
78
|
exports.fillTextDatepickerInput = fillTextDatepickerInput;
|
|
79
|
+
exports.isDateSegmentAtSelector = isDateSegmentAtSelector;
|
|
77
80
|
exports.verifyFillReadback = verifyFillReadback;
|
|
78
81
|
exports.verifyPromptSelectorCommitted = verifyPromptSelectorCommitted;
|
|
79
82
|
exports.tailRetargetPhrasesForStep = tailRetargetPhrasesForStep;
|
|
80
83
|
exports.stepQuotesValueNotControlName = stepQuotesValueNotControlName;
|
|
84
|
+
exports.waitForUploadNetworkSignal = waitForUploadNetworkSignal;
|
|
81
85
|
exports.isUploadAffordanceLabel = isUploadAffordanceLabel;
|
|
82
86
|
exports.writeFixtureToTempFile = writeFixtureToTempFile;
|
|
83
87
|
exports.attachToSurfacedInput = attachToSurfacedInput;
|
|
@@ -91,12 +95,15 @@ exports.parseSegmentedDateValue = parseSegmentedDateValue;
|
|
|
91
95
|
exports.resolveSegmentedDateGroup = resolveSegmentedDateGroup;
|
|
92
96
|
exports.parseSegmentedPartStep = parseSegmentedPartStep;
|
|
93
97
|
exports.trySegmentedDateSpinbuttonPrimitive = trySegmentedDateSpinbuttonPrimitive;
|
|
98
|
+
exports.runSegmentedDateSpinbuttonPrimitive = runSegmentedDateSpinbuttonPrimitive;
|
|
94
99
|
exports.parseRadioStep = parseRadioStep;
|
|
95
100
|
exports.parseWidgetOptionClickStep = parseWidgetOptionClickStep;
|
|
96
101
|
exports.parseCheckStep = parseCheckStep;
|
|
97
102
|
exports.parseCheckStateStep = parseCheckStateStep;
|
|
98
103
|
exports.pollEnumerate = pollEnumerate;
|
|
99
104
|
exports.waitForTransitionBody = waitForTransitionBody;
|
|
105
|
+
exports.settleAndSnapshotPostClick = settleAndSnapshotPostClick;
|
|
106
|
+
exports.readMidFlowPostClickAdvance = readMidFlowPostClickAdvance;
|
|
100
107
|
exports.injectCaptchaTokenAndSubmit = injectCaptchaTokenAndSubmit;
|
|
101
108
|
exports.submitCaptchaGatedForm = submitCaptchaGatedForm;
|
|
102
109
|
exports.trySelectPrimitive = trySelectPrimitive;
|
|
@@ -157,6 +164,8 @@ const errors_2 = require("./errors");
|
|
|
157
164
|
const frame_target_1 = require("./frame-target");
|
|
158
165
|
const phantom_click_1 = require("./phantom-click");
|
|
159
166
|
const quoted_spans_1 = require("./quoted-spans");
|
|
167
|
+
const runtime_capture_store_1 = require("./runtime-capture-store");
|
|
168
|
+
const segmented_date_spinbutton_1 = require("./segmented-date-spinbutton");
|
|
160
169
|
const session_teardown_1 = require("./session-teardown");
|
|
161
170
|
const spa_readiness_1 = require("./spa-readiness");
|
|
162
171
|
const stagehand_guard_1 = require("./stagehand-guard");
|
|
@@ -165,7 +174,6 @@ const target_resolution_diagnostic_1 = require("./target-resolution-diagnostic")
|
|
|
165
174
|
const trusted_click_1 = require("./trusted-click");
|
|
166
175
|
const upload_affordance_expr_1 = require("./upload-affordance-expr");
|
|
167
176
|
const watchdog_1 = require("./watchdog");
|
|
168
|
-
const recon_shared_1 = require("../scripts/recon-shared");
|
|
169
177
|
const client_1 = require("../testmail/client");
|
|
170
178
|
var spa_readiness_2 = require("./spa-readiness");
|
|
171
179
|
Object.defineProperty(exports, "waitForSpaReady", { enumerable: true, get: function () { return spa_readiness_2.waitForSpaReady; } });
|
|
@@ -393,6 +401,8 @@ function wireSignalCapture(page, params) {
|
|
|
393
401
|
method: capture.method,
|
|
394
402
|
status: capture.status,
|
|
395
403
|
url: capture.url,
|
|
404
|
+
hasBody: (req.requestPostData ?? "").length > 0,
|
|
405
|
+
multipart: /^multipart\//i.test(requestContentType(req.requestHeaders)),
|
|
396
406
|
});
|
|
397
407
|
if (recentCaptureMeta.length > RECENT_CAPTURES_WINDOW) {
|
|
398
408
|
recentCaptureMeta.splice(0, recentCaptureMeta.length - RECENT_CAPTURES_WINDOW);
|
|
@@ -1106,7 +1116,9 @@ function prepareFailureDumpBody(raw) {
|
|
|
1106
1116
|
function ariaCheckedReadExpr(elVar) {
|
|
1107
1117
|
return `${elVar}.getAttribute("aria-checked")`;
|
|
1108
1118
|
}
|
|
1109
|
-
|
|
1119
|
+
/** Bound on {@link StepSnapshot.visibleText}, keeping a pathological page from bloating snapshots. */
|
|
1120
|
+
const VISIBLE_TEXT_EXCERPT_CHARS = 3000;
|
|
1121
|
+
const DOM_SNAPSHOT_EXPR = `(() => { const b = document.body; if (!b) return { html: 0, text: "", values: "", state: "", full: "", digest: "" }; 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; const norm = t.replace(/\\s+/g, " ").trim(); let th = 2166136261; for (let tk = 0; tk < norm.length; tk++) { th ^= norm.charCodeAt(tk); th = Math.imul(th, 16777619); } return { html: (b.outerHTML || "").length, text: t.length + ":" + t.slice(0, 200), values, state, full: norm.slice(0, ${VISIBLE_TEXT_EXCERPT_CHARS}), digest: (th >>> 0).toString(36) + ":" + norm.length }; })()`;
|
|
1110
1122
|
/** Caps mirroring {@link DOM_SNAPSHOT_EXPR}'s truncation so a pathological page can't bloat the failure bundle. */
|
|
1111
1123
|
const FIELD_VALUES_AT_FAILURE_MAX_CONTROLS = 200;
|
|
1112
1124
|
const FIELD_VALUES_AT_FAILURE_MAX_VALUE_LENGTH = 500;
|
|
@@ -1156,13 +1168,49 @@ function fieldValuesAtFailureExpr() {
|
|
|
1156
1168
|
return out;
|
|
1157
1169
|
})()`;
|
|
1158
1170
|
}
|
|
1171
|
+
/**
|
|
1172
|
+
* Reads the in-page quiescence probe as a typed, non-null value. A detached
|
|
1173
|
+
* frame or malformed probe result reads as idle so settle consumes the reader
|
|
1174
|
+
* directly and needs no guard of its own.
|
|
1175
|
+
*/
|
|
1176
|
+
async function readPageQuiescence(target) {
|
|
1177
|
+
const idle = { pending: 0, mutations: 0 };
|
|
1178
|
+
const result = await Promise.resolve(target.evaluate(click_settle_1.QUIESCENCE_PROBE_EXPR)).catch(() => idle);
|
|
1179
|
+
if (result === null || typeof result !== "object")
|
|
1180
|
+
return idle;
|
|
1181
|
+
const { pending, mutations } = result;
|
|
1182
|
+
return typeof pending === "number" && typeof mutations === "number"
|
|
1183
|
+
? { pending, mutations }
|
|
1184
|
+
: idle;
|
|
1185
|
+
}
|
|
1186
|
+
/**
|
|
1187
|
+
* Installs the quiescence probe before a click, because it only starts counting
|
|
1188
|
+
* requests and mutations from its first read and a post-click install would miss
|
|
1189
|
+
* everything the click triggers immediately.
|
|
1190
|
+
*/
|
|
1191
|
+
async function armPageQuiescence(target) {
|
|
1192
|
+
await readPageQuiescence(target);
|
|
1193
|
+
}
|
|
1194
|
+
/**
|
|
1195
|
+
* Reads only the visible-text key settle polls on. Going through
|
|
1196
|
+
* {@link snapshotPage} also read the frame URL, which rejects once a submit
|
|
1197
|
+
* detaches the OOPIF and would abort the settle mid-poll; a failed read here
|
|
1198
|
+
* degrades to the empty signature like the snapshot's own text read does.
|
|
1199
|
+
*/
|
|
1200
|
+
async function readSettleTextSignature(target) {
|
|
1201
|
+
const result = await Promise.resolve(target.evaluate(DOM_SNAPSHOT_EXPR)).catch(() => null);
|
|
1202
|
+
if (result === null || typeof result !== "object")
|
|
1203
|
+
return "";
|
|
1204
|
+
const { text, digest } = result;
|
|
1205
|
+
return (0, click_settle_1.settleTextKey)(typeof text === "string" ? text : "", typeof digest === "string" ? digest : undefined);
|
|
1206
|
+
}
|
|
1159
1207
|
/**
|
|
1160
1208
|
* Settles after a click using the target's own text signature and in-page
|
|
1161
1209
|
* quiescence reader, so every post-click site decides from the same settle
|
|
1162
1210
|
* signal instead of hand-building the readers.
|
|
1163
1211
|
*/
|
|
1164
|
-
async function settleWithPageQuiescence(page, target, signalCounter, preNetworkCount, submitShaped, preVisibleTextSignature) {
|
|
1165
|
-
await (0, click_settle_1.settleAfterClick)(page, signalCounter, preNetworkCount, submitShaped,
|
|
1212
|
+
async function settleWithPageQuiescence(page, target, signalCounter, preNetworkCount, submitShaped, preVisibleTextSignature, preVisibleTextDigest) {
|
|
1213
|
+
await (0, click_settle_1.settleAfterClick)(page, signalCounter, preNetworkCount, submitShaped, () => readSettleTextSignature(target), (0, click_settle_1.settleTextKey)(preVisibleTextSignature, preVisibleTextDigest), () => readPageQuiescence(target));
|
|
1166
1214
|
}
|
|
1167
1215
|
/**
|
|
1168
1216
|
* Captures the pre/post signal triple the submit-verify cascade diffs.
|
|
@@ -1181,6 +1229,8 @@ async function settleWithPageQuiescence(page, target, signalCounter, preNetworkC
|
|
|
1181
1229
|
async function snapshotPage(target, signalCounter, page, captureSelectionState = false) {
|
|
1182
1230
|
let bodyHtmlLength = 0;
|
|
1183
1231
|
let visibleTextSignature = "";
|
|
1232
|
+
let visibleText = "";
|
|
1233
|
+
let visibleTextDigest = "";
|
|
1184
1234
|
let formValueSignature = "";
|
|
1185
1235
|
let selectionStateSignature = "";
|
|
1186
1236
|
try {
|
|
@@ -1193,6 +1243,10 @@ async function snapshotPage(target, signalCounter, page, captureSelectionState =
|
|
|
1193
1243
|
typeof result.text === "string") {
|
|
1194
1244
|
bodyHtmlLength = result.html;
|
|
1195
1245
|
visibleTextSignature = result.text;
|
|
1246
|
+
const rawFull = result.full;
|
|
1247
|
+
visibleText = typeof rawFull === "string" ? rawFull : "";
|
|
1248
|
+
const rawDigest = result.digest;
|
|
1249
|
+
visibleTextDigest = typeof rawDigest === "string" ? rawDigest : "";
|
|
1196
1250
|
const rawValues = result.values;
|
|
1197
1251
|
formValueSignature = typeof rawValues === "string" ? rawValues : "";
|
|
1198
1252
|
const rawState = result.state;
|
|
@@ -1235,6 +1289,8 @@ async function snapshotPage(target, signalCounter, page, captureSelectionState =
|
|
|
1235
1289
|
url,
|
|
1236
1290
|
bodyHtmlLength,
|
|
1237
1291
|
visibleTextSignature,
|
|
1292
|
+
visibleText,
|
|
1293
|
+
visibleTextDigest: visibleTextDigest === "" ? undefined : visibleTextDigest,
|
|
1238
1294
|
formValueSignature,
|
|
1239
1295
|
selectionStateSignature,
|
|
1240
1296
|
selectionStateByXpath,
|
|
@@ -1534,12 +1590,20 @@ function isStepSettleWaitShaped(params) {
|
|
|
1534
1590
|
return isStepSubmitShaped(params) || isAdvanceStep(params.step);
|
|
1535
1591
|
}
|
|
1536
1592
|
/**
|
|
1537
|
-
*
|
|
1593
|
+
* The single definition of a genuine final submit: the last slot of a flow
|
|
1594
|
+
* that has submit semantics. A final slot without them is mid-flow.
|
|
1595
|
+
*/
|
|
1596
|
+
function isGenuineFinalSubmit(params) {
|
|
1597
|
+
return params.isFinalStep && params.flowHasSubmitSemantics;
|
|
1598
|
+
}
|
|
1599
|
+
/**
|
|
1600
|
+
* Submit-shaped but NOT the flow's genuine final submit (a final slot without
|
|
1601
|
+
* submit semantics, e.g. after a replan trims the tail, is still mid-flow): a mid-flow advancing submit
|
|
1538
1602
|
* (e.g. account creation) that can be credited on a demonstrable page advance
|
|
1539
1603
|
* instead of needing the final submit's network/URL proof.
|
|
1540
1604
|
*/
|
|
1541
1605
|
function isMidFlowSubmitShapedStep(params) {
|
|
1542
|
-
if (params
|
|
1606
|
+
if (isGenuineFinalSubmit(params))
|
|
1543
1607
|
return false;
|
|
1544
1608
|
return isStepSubmitShaped(params) || params.elementSubmitShaped === true;
|
|
1545
1609
|
}
|
|
@@ -1669,15 +1733,7 @@ function latestCaptureIndex(recentCaptures) {
|
|
|
1669
1733
|
* so callers scan in capture order. Returns [] when the dir is unreadable.
|
|
1670
1734
|
*/
|
|
1671
1735
|
function capturesAfterIndex(preIdx, capturesDir) {
|
|
1672
|
-
|
|
1673
|
-
try {
|
|
1674
|
-
entries = (0, node_fs_1.readdirSync)(capturesDir);
|
|
1675
|
-
}
|
|
1676
|
-
catch {
|
|
1677
|
-
return [];
|
|
1678
|
-
}
|
|
1679
|
-
return entries
|
|
1680
|
-
.filter((f) => f.endsWith(".json") && !f.endsWith(".decoded.json"))
|
|
1736
|
+
return (0, runtime_capture_store_1.listCaptureFiles)(capturesDir)
|
|
1681
1737
|
.map((f) => ({ f, idx: parseCaptureIndex(f) }))
|
|
1682
1738
|
.filter((e) => e.idx !== null && e.idx > preIdx)
|
|
1683
1739
|
.sort((a, b) => a.idx - b.idx)
|
|
@@ -1708,7 +1764,7 @@ function windowHasTransitionBody(params) {
|
|
|
1708
1764
|
const { preIdx, advanceTransitionBodyPattern } = params;
|
|
1709
1765
|
if (!advanceTransitionBodyPattern)
|
|
1710
1766
|
return false;
|
|
1711
|
-
const capturesDir = params.capturesDir ?? (0,
|
|
1767
|
+
const capturesDir = params.capturesDir ?? (0, runtime_capture_store_1.defaultCapturesDir)();
|
|
1712
1768
|
let rx;
|
|
1713
1769
|
try {
|
|
1714
1770
|
rx = new RegExp(advanceTransitionBodyPattern);
|
|
@@ -1718,8 +1774,9 @@ function windowHasTransitionBody(params) {
|
|
|
1718
1774
|
}
|
|
1719
1775
|
for (const filename of capturesAfterIndex(preIdx, capturesDir)) {
|
|
1720
1776
|
try {
|
|
1721
|
-
const
|
|
1722
|
-
|
|
1777
|
+
const capture = (0, runtime_capture_store_1.readCaptureFile)(capturesDir, filename);
|
|
1778
|
+
if (!capture)
|
|
1779
|
+
continue;
|
|
1723
1780
|
const body = typeof capture.requestPostData === "string"
|
|
1724
1781
|
? capture.requestPostData
|
|
1725
1782
|
: JSON.stringify(capture.requestPostData ?? "");
|
|
@@ -1750,7 +1807,7 @@ function windowHasAdvanceTransition(params) {
|
|
|
1750
1807
|
const { preIdx, advanceTransitionBodyPattern } = params;
|
|
1751
1808
|
if (!advanceTransitionBodyPattern)
|
|
1752
1809
|
return false;
|
|
1753
|
-
const capturesDir = params.capturesDir ?? (0,
|
|
1810
|
+
const capturesDir = params.capturesDir ?? (0, runtime_capture_store_1.defaultCapturesDir)();
|
|
1754
1811
|
let rx;
|
|
1755
1812
|
try {
|
|
1756
1813
|
rx = new RegExp(advanceTransitionBodyPattern);
|
|
@@ -1760,8 +1817,9 @@ function windowHasAdvanceTransition(params) {
|
|
|
1760
1817
|
}
|
|
1761
1818
|
for (const filename of capturesAfterIndex(preIdx, capturesDir)) {
|
|
1762
1819
|
try {
|
|
1763
|
-
const
|
|
1764
|
-
|
|
1820
|
+
const capture = (0, runtime_capture_store_1.readCaptureFile)(capturesDir, filename);
|
|
1821
|
+
if (!capture)
|
|
1822
|
+
continue;
|
|
1765
1823
|
const body = typeof capture.requestPostData === "string"
|
|
1766
1824
|
? capture.requestPostData
|
|
1767
1825
|
: JSON.stringify(capture.requestPostData ?? "");
|
|
@@ -1831,13 +1889,29 @@ function isDomOnlyAdvanceVerified(params) {
|
|
|
1831
1889
|
* rejection, so strong structural click signals (a real network advance or a
|
|
1832
1890
|
* demonstrable page advance) credit the step instead of failing it and
|
|
1833
1891
|
* re-clicking at the same position. Final steps and weak-signal cases return
|
|
1834
|
-
* false and keep the DOM-state fallback / failure path.
|
|
1892
|
+
* false and keep the DOM-state fallback / failure path. Invalid markers
|
|
1893
|
+
* describe the page the click landed on, so they only veto a network-only
|
|
1894
|
+
* credit, never a settled page advance.
|
|
1835
1895
|
*/
|
|
1836
1896
|
function isNullVerdictAdvanceCredited(params) {
|
|
1837
|
-
if (!params.midFlowSubmit || !params.judgeUnavailable
|
|
1897
|
+
if (!params.midFlowSubmit || !params.judgeUnavailable)
|
|
1838
1898
|
return false;
|
|
1839
|
-
|
|
1840
|
-
|
|
1899
|
+
if (params.pageAdvanced)
|
|
1900
|
+
return true;
|
|
1901
|
+
return params.networkAdvanced && params.invalidMarkerCount <= 0;
|
|
1902
|
+
}
|
|
1903
|
+
/**
|
|
1904
|
+
* Single decision both post-click judge sites use to credit a mid-flow submit
|
|
1905
|
+
* on the page's own demonstrable advance, so a stale title or missing network
|
|
1906
|
+
* the judge reads with final-submit eyes cannot undo it. `signalAdvanced` is the
|
|
1907
|
+
* site's own settled-advance signal (the primary click's view-swap credit, the
|
|
1908
|
+
* fallback click's effective verdict); the null-verdict path covers an
|
|
1909
|
+
* unavailable judge.
|
|
1910
|
+
*/
|
|
1911
|
+
function isMidFlowAdvanceCredited(params) {
|
|
1912
|
+
if (params.midFlowSubmit && params.signalAdvanced)
|
|
1913
|
+
return true;
|
|
1914
|
+
return isNullVerdictAdvanceCredited(params);
|
|
1841
1915
|
}
|
|
1842
1916
|
/**
|
|
1843
1917
|
* Client-side view-swap verification gate. Credits a click that produces
|
|
@@ -1930,7 +2004,7 @@ function isNullVerdictAdvanceCredited(params) {
|
|
|
1930
2004
|
*
|
|
1931
2005
|
* **Mid-flow submit:** `midFlowSubmit` (see {@link isMidFlowSubmitShapedStep})
|
|
1932
2006
|
* lets a submit-shaped, non-final step be credited when `pageAdvanced` (the
|
|
1933
|
-
* shared {@link
|
|
2007
|
+
* shared {@link hasSettledPageAdvanced} judgement of the visible text; defaults to
|
|
1934
2008
|
* `textChanged` when omitted) holds — byte delta is not required, since a
|
|
1935
2009
|
* wizard swap can keep the HTML size — with the same invalid-marker and
|
|
1936
2010
|
* destination vetoes. The clicked control being gone is NOT a veto here: a
|
|
@@ -1953,7 +2027,6 @@ function isClickViewSwapVerified(params) {
|
|
|
1953
2027
|
midFlowActRan &&
|
|
1954
2028
|
destinationPlausible !== false &&
|
|
1955
2029
|
!isAdvanceWithPattern &&
|
|
1956
|
-
invalidMarkerDelta <= 0 &&
|
|
1957
2030
|
textChanged &&
|
|
1958
2031
|
pageAdvanced) {
|
|
1959
2032
|
return true;
|
|
@@ -2071,7 +2144,7 @@ async function countNgInvalidContainers(target) {
|
|
|
2071
2144
|
function describeAttemptEffectSignals(pre, post, recentCaptureMeta, preMetaLength) {
|
|
2072
2145
|
const bytesDelta = post.bodyHtmlLength - pre.bodyHtmlLength;
|
|
2073
2146
|
const networkDelta = post.networkCount - pre.networkCount;
|
|
2074
|
-
const textChanged =
|
|
2147
|
+
const textChanged = (0, phantom_click_1.hasVisibleTextChanged)(pre, post);
|
|
2075
2148
|
const valueChanged = post.formValueSignature !== pre.formValueSignature;
|
|
2076
2149
|
const selectionStateChanged = post.selectionStateSignature !== pre.selectionStateSignature;
|
|
2077
2150
|
const windowCaptures = recentCaptureMeta.slice(preMetaLength);
|
|
@@ -2212,9 +2285,11 @@ function shouldSkipTechnique(params) {
|
|
|
2212
2285
|
* navigations, partial DOM reflows, etc.).
|
|
2213
2286
|
*/
|
|
2214
2287
|
function isSubmitRevealedInvalid(params) {
|
|
2215
|
-
const { isFinalStep, requireSubmitEndpoint, resolvedMethod, effectSignals, preSubmitInvalidCount, postAttemptInvalidCount, } = params;
|
|
2288
|
+
const { isFinalStep, requireSubmitEndpoint, resolvedMethod, effectSignals, preSubmitInvalidCount, postAttemptInvalidCount, midFlowAdvanced = false, } = params;
|
|
2216
2289
|
if (!isFinalStep)
|
|
2217
2290
|
return false;
|
|
2291
|
+
if (midFlowAdvanced)
|
|
2292
|
+
return false;
|
|
2218
2293
|
if (!requireSubmitEndpoint)
|
|
2219
2294
|
return false;
|
|
2220
2295
|
if (resolvedMethod !== "click")
|
|
@@ -2905,24 +2980,36 @@ const STEP_REJUDGE_COMPARE_CHARS = 1200;
|
|
|
2905
2980
|
*/
|
|
2906
2981
|
async function judgeStepOutcome(params) {
|
|
2907
2982
|
const { client, captureFn, step, isFinalStep, target } = params;
|
|
2908
|
-
|
|
2983
|
+
const genuineFinal = isGenuineFinalSubmit({
|
|
2984
|
+
isFinalStep,
|
|
2985
|
+
flowHasSubmitSemantics: params.flowHasSubmitSemantics,
|
|
2986
|
+
});
|
|
2987
|
+
if (genuineFinal) {
|
|
2909
2988
|
return (0, verify_submit_1.verifySubmitWithLLM)({
|
|
2910
2989
|
client,
|
|
2911
2990
|
input: {
|
|
2912
|
-
...(await buildSubmitJudgeStepContext({ step, isFinalStep, target })),
|
|
2991
|
+
...(await buildSubmitJudgeStepContext({ step, isFinalStep: genuineFinal, target })),
|
|
2913
2992
|
...params.submitInput,
|
|
2914
2993
|
},
|
|
2915
2994
|
captureFn,
|
|
2916
2995
|
});
|
|
2917
2996
|
}
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2997
|
+
const hasSettledRead = params.settledPostVisibleText !== undefined;
|
|
2998
|
+
if (!hasSettledRead) {
|
|
2999
|
+
await settleWithPageQuiescence(params.page, target, params.signalCounter, params.preNetworkCount, isStepSettleWaitShaped({
|
|
3000
|
+
step,
|
|
3001
|
+
submitStep: params.submitStep,
|
|
3002
|
+
isFinalStep,
|
|
3003
|
+
flowHasSubmitSemantics: params.flowHasSubmitSemantics,
|
|
3004
|
+
}) || params.elementSubmitShaped === true, params.preVisibleTextSignature, params.preVisibleTextDigest);
|
|
3005
|
+
}
|
|
3006
|
+
const signatureColon = params.preVisibleTextSignature.indexOf(":");
|
|
3007
|
+
const signatureHead = (signatureColon === -1
|
|
3008
|
+
? params.preVisibleTextSignature
|
|
3009
|
+
: params.preVisibleTextSignature.slice(signatureColon + 1))
|
|
2924
3010
|
.replace(/\s+/g, " ")
|
|
2925
3011
|
.trim();
|
|
3012
|
+
const beforeText = params.preVisibleText ? params.preVisibleText : signatureHead;
|
|
2926
3013
|
const judgeText = (afterText) => (0, verify_step_advanced_1.verifyStepAdvancedWithLLM)({
|
|
2927
3014
|
client,
|
|
2928
3015
|
input: {
|
|
@@ -2934,7 +3021,7 @@ async function judgeStepOutcome(params) {
|
|
|
2934
3021
|
captureFn,
|
|
2935
3022
|
});
|
|
2936
3023
|
const rejudgeIfTextMoved = async (first, judgedText) => {
|
|
2937
|
-
if (first === null || first.advanced)
|
|
3024
|
+
if (first === null || first.advanced || hasSettledRead)
|
|
2938
3025
|
return first;
|
|
2939
3026
|
await params.page.waitForTimeout(STEP_REJUDGE_DELAY_MS);
|
|
2940
3027
|
const laterText = await probeVisibleTextForJudge(target);
|
|
@@ -2944,7 +3031,9 @@ async function judgeStepOutcome(params) {
|
|
|
2944
3031
|
}
|
|
2945
3032
|
return (await judgeText(laterText)) ?? first;
|
|
2946
3033
|
};
|
|
2947
|
-
const afterText =
|
|
3034
|
+
const afterText = hasSettledRead
|
|
3035
|
+
? (params.settledPostVisibleText ?? "")
|
|
3036
|
+
: await probeVisibleTextForJudge(target);
|
|
2948
3037
|
const firstVerdict = await judgeText(afterText);
|
|
2949
3038
|
const verdict = await rejudgeIfTextMoved(firstVerdict, afterText);
|
|
2950
3039
|
if (verdict === null)
|
|
@@ -3058,7 +3147,7 @@ function extractSubmitFailureEvidence(recentCaptureFilenames,
|
|
|
3058
3147
|
* deterministic hostname equality. Empty list / "any-4xx" mode disables
|
|
3059
3148
|
* the host filter and returns any 4xx in the window.
|
|
3060
3149
|
*/
|
|
3061
|
-
ownBackendHostnames, capturesDir = (0,
|
|
3150
|
+
ownBackendHostnames, capturesDir = (0, runtime_capture_store_1.defaultCapturesDir)(), mode = "strict") {
|
|
3062
3151
|
if (recentCaptureFilenames.length === 0)
|
|
3063
3152
|
return "";
|
|
3064
3153
|
if (mode === "strict" && ownBackendHostnames.length === 0)
|
|
@@ -3070,9 +3159,9 @@ ownBackendHostnames, capturesDir = (0, recon_shared_1.resolveReconRunDir)().grap
|
|
|
3070
3159
|
continue;
|
|
3071
3160
|
seen.add(filename);
|
|
3072
3161
|
try {
|
|
3073
|
-
const
|
|
3074
|
-
|
|
3075
|
-
|
|
3162
|
+
const capture = (0, runtime_capture_store_1.readCaptureFile)(capturesDir, filename);
|
|
3163
|
+
if (!capture)
|
|
3164
|
+
continue;
|
|
3076
3165
|
if (typeof capture.url !== "string")
|
|
3077
3166
|
continue;
|
|
3078
3167
|
if (mode === "strict") {
|
|
@@ -3149,7 +3238,7 @@ ownBackendHostnames, capturesDir = (0, recon_shared_1.resolveReconRunDir)().grap
|
|
|
3149
3238
|
* Returns a numbered evidence list. Empty string when no GA collect
|
|
3150
3239
|
* captures are present. Advisory — never load-bearing.
|
|
3151
3240
|
*/
|
|
3152
|
-
function extractGaEventEvidence(recentCaptureFilenames, capturesDir = (0,
|
|
3241
|
+
function extractGaEventEvidence(recentCaptureFilenames, capturesDir = (0, runtime_capture_store_1.defaultCapturesDir)()) {
|
|
3153
3242
|
if (recentCaptureFilenames.length === 0)
|
|
3154
3243
|
return "";
|
|
3155
3244
|
const records = [];
|
|
@@ -3159,9 +3248,9 @@ function extractGaEventEvidence(recentCaptureFilenames, capturesDir = (0, recon_
|
|
|
3159
3248
|
continue;
|
|
3160
3249
|
seen.add(filename);
|
|
3161
3250
|
try {
|
|
3162
|
-
const
|
|
3163
|
-
|
|
3164
|
-
|
|
3251
|
+
const capture = (0, runtime_capture_store_1.readCaptureFile)(capturesDir, filename);
|
|
3252
|
+
if (!capture)
|
|
3253
|
+
continue;
|
|
3165
3254
|
if (typeof capture.url !== "string")
|
|
3166
3255
|
continue;
|
|
3167
3256
|
let url;
|
|
@@ -3533,6 +3622,25 @@ async function fillTextDatepickerInput(target, selector, value) {
|
|
|
3533
3622
|
return { filled: false, postValue: "", strategy: "none" };
|
|
3534
3623
|
}
|
|
3535
3624
|
}
|
|
3625
|
+
/**
|
|
3626
|
+
* Pre-write check for the observe-act fill override: a lone-segment write
|
|
3627
|
+
* (insertText) cannot compose a date, so callers refuse before any write.
|
|
3628
|
+
* An evaluate error propagates so the attempt fails rather than writing blind;
|
|
3629
|
+
* only a selector form with no XPath body (nothing to resolve) answers false.
|
|
3630
|
+
*/
|
|
3631
|
+
async function isDateSegmentAtSelector(target, selector) {
|
|
3632
|
+
const xpath = xpathBodyForEvaluate(selector);
|
|
3633
|
+
if (xpath === null)
|
|
3634
|
+
return false;
|
|
3635
|
+
const expr = `(() => {
|
|
3636
|
+
const result = document.evaluate(${JSON.stringify(xpath)}, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
|
|
3637
|
+
const el = result.singleNodeValue;
|
|
3638
|
+
if (!el) return false;
|
|
3639
|
+
${segmented_date_spinbutton_1.SEGMENT_OWNER_JS}
|
|
3640
|
+
return isDateSegment(el);
|
|
3641
|
+
})()`;
|
|
3642
|
+
return (await target.evaluate(expr)) === true;
|
|
3643
|
+
}
|
|
3536
3644
|
/**
|
|
3537
3645
|
* Read back an element's value after a fill action and compare to the
|
|
3538
3646
|
* expected value. Catches silent-value-rejection cases that the verifier's
|
|
@@ -3570,7 +3678,7 @@ async function verifyFillReadback(target, selector, expectedValue) {
|
|
|
3570
3678
|
if (!el) return null;
|
|
3571
3679
|
const tag = el.tagName ? el.tagName.toLowerCase() : "";
|
|
3572
3680
|
let actual = "";
|
|
3573
|
-
${SEGMENT_OWNER_JS}
|
|
3681
|
+
${segmented_date_spinbutton_1.SEGMENT_OWNER_JS}
|
|
3574
3682
|
if (isDateSegment(el)) {
|
|
3575
3683
|
return { outcome: "segment", postValue: el.value || el.getAttribute("aria-valuenow") || "", tag };
|
|
3576
3684
|
}
|
|
@@ -4207,6 +4315,47 @@ async function resolvedClickTargetIsSubmitShaped(target, selector, xpathTail = n
|
|
|
4207
4315
|
return false;
|
|
4208
4316
|
}
|
|
4209
4317
|
}
|
|
4318
|
+
/**
|
|
4319
|
+
* Canonical key for an absolute xpath so Stagehand's `/html/body/div/button`
|
|
4320
|
+
* (index omitted when sole of its tag) and this module's `/html[1]/body[1]/
|
|
4321
|
+
* div[1]/button[1]` compare equal: a node's first-of-tag and sole-of-tag
|
|
4322
|
+
* forms can never coexist under one parent, so dropping `[1]` is injective.
|
|
4323
|
+
*/
|
|
4324
|
+
function canonicalAbsoluteXpath(selector) {
|
|
4325
|
+
const body = xpathBodyForEvaluate(selector);
|
|
4326
|
+
return body?.startsWith("/") ? body.replace(/\[1\]/g, "").toLowerCase() : null;
|
|
4327
|
+
}
|
|
4328
|
+
/**
|
|
4329
|
+
* Canonical xpaths of every control on the page that is submit-shaped right
|
|
4330
|
+
* now, read before an act clicks anything. The act performs the click itself,
|
|
4331
|
+
* and a view swap removes the control with the page, so the only read that
|
|
4332
|
+
* cannot be blinded by the transition is one taken beforehand — and one that
|
|
4333
|
+
* does not depend on observe having resolved the same control the act later
|
|
4334
|
+
* clicks. Empty on any read failure: absence never manufactures a shape.
|
|
4335
|
+
*/
|
|
4336
|
+
async function readSubmitShapedControlXpaths(target) {
|
|
4337
|
+
const expr = `(() => {
|
|
4338
|
+
const xpathOf = ${XPATH_OF_FN_SRC};
|
|
4339
|
+
const isSubmitShaped = ${submit_control_1.SUBMIT_SHAPE_EXPR};
|
|
4340
|
+
const out = [];
|
|
4341
|
+
for (const el of document.querySelectorAll("button, input, a, [role='button'], [type='submit'], [onclick], [tabindex]")) {
|
|
4342
|
+
if (isSubmitShaped(el)) out.push(xpathOf(el));
|
|
4343
|
+
}
|
|
4344
|
+
return out;
|
|
4345
|
+
})()`;
|
|
4346
|
+
try {
|
|
4347
|
+
const result = await target.evaluate(expr);
|
|
4348
|
+
if (!Array.isArray(result))
|
|
4349
|
+
return new Set();
|
|
4350
|
+
return new Set(result
|
|
4351
|
+
.filter((x) => typeof x === "string")
|
|
4352
|
+
.map(canonicalAbsoluteXpath)
|
|
4353
|
+
.filter((x) => x !== null));
|
|
4354
|
+
}
|
|
4355
|
+
catch {
|
|
4356
|
+
return new Set();
|
|
4357
|
+
}
|
|
4358
|
+
}
|
|
4210
4359
|
/**
|
|
4211
4360
|
* True when the primary (Stagehand-recorded) xpath is unresolvable and only
|
|
4212
4361
|
* `xpathTailForRetarget`'s loose tag+same-tag-sibling-position re-anchor
|
|
@@ -4523,12 +4672,17 @@ const UPLOAD_URL_PATTERNS = [
|
|
|
4523
4672
|
"/media",
|
|
4524
4673
|
"/import",
|
|
4525
4674
|
];
|
|
4675
|
+
/** Case-insensitive Content-Type lookup; CDP preserves the page's header casing. */
|
|
4676
|
+
function requestContentType(headers) {
|
|
4677
|
+
const key = Object.keys(headers).find((k) => k.toLowerCase() === "content-type");
|
|
4678
|
+
return key ? (headers[key] ?? "") : "";
|
|
4679
|
+
}
|
|
4526
4680
|
/**
|
|
4527
4681
|
* Poll the recent-capture window for an upload-related POST after a file has
|
|
4528
4682
|
* been attached. Extracted from `tryUploadPrimitive` so the raw-input path,
|
|
4529
4683
|
* the click-to-surface path, and the CDP native-chooser path all verify the
|
|
4530
|
-
* same way. Returns true as soon as a non-GET capture
|
|
4531
|
-
* {@link UPLOAD_URL_PATTERNS} lands; false if the timeout elapses first.
|
|
4684
|
+
* same way. Returns true as soon as a non-GET capture that is multipart, or is not
|
|
4685
|
+
* known to be body-less and has a URL matching {@link UPLOAD_URL_PATTERNS}, lands; false if the timeout elapses first.
|
|
4532
4686
|
*/
|
|
4533
4687
|
async function waitForUploadNetworkSignal(params) {
|
|
4534
4688
|
const { page, fixture, logger, signalCounter, recentCaptureMeta, stopWhen } = params;
|
|
@@ -4541,6 +4695,10 @@ async function waitForUploadNetworkSignal(params) {
|
|
|
4541
4695
|
const uploadCapture = newCaptures.find((cap) => {
|
|
4542
4696
|
if (cap.method === "GET")
|
|
4543
4697
|
return false;
|
|
4698
|
+
if (cap.multipart)
|
|
4699
|
+
return true;
|
|
4700
|
+
if (cap.hasBody === false)
|
|
4701
|
+
return false;
|
|
4544
4702
|
const lowerUrl = cap.url.toLowerCase();
|
|
4545
4703
|
return UPLOAD_URL_PATTERNS.some((p) => lowerUrl.includes(p));
|
|
4546
4704
|
});
|
|
@@ -4672,8 +4830,9 @@ async function tryUploadPrimitive(params) {
|
|
|
4672
4830
|
* Attach the fixture to an already-surfaced `<input type=file>` (raw or freshly
|
|
4673
4831
|
* mounted after a click) and verify. Extracted verbatim from the original
|
|
4674
4832
|
* `tryUploadPrimitive` body so the raw-input path and the click-to-surface
|
|
4675
|
-
* path share one setInputFiles + framework-change-dispatch + network/DOM verify
|
|
4676
|
-
*
|
|
4833
|
+
* path share one setInputFiles + framework-change-dispatch + network/DOM verify implementation.
|
|
4834
|
+
* Returns true only on a verified signal
|
|
4835
|
+
* (upload POST or populated `.files`).
|
|
4677
4836
|
*/
|
|
4678
4837
|
async function attachToSurfacedInput(params) {
|
|
4679
4838
|
const { page, target, fixture, logger, signalCounter, recentCaptureMeta } = params;
|
|
@@ -4740,18 +4899,10 @@ async function attachToSurfacedInput(params) {
|
|
|
4740
4899
|
.evaluate("(() => { const els = document.querySelectorAll('input[type=file]'); for (const el of els) { if (el.files && el.files.length > 0) return el.files.length; } return 0; })()")
|
|
4741
4900
|
.catch(() => 0);
|
|
4742
4901
|
if (typeof attachedLength !== "number" || attachedLength === 0) {
|
|
4743
|
-
|
|
4744
|
-
//
|
|
4745
|
-
//
|
|
4746
|
-
|
|
4747
|
-
// a DataTransfer fires on the visible drop area — they don't observe
|
|
4748
|
-
// the hidden input's `files[]` mutations even with synthetic `change`
|
|
4749
|
-
// dispatches. This is the documented Playwright community workaround.
|
|
4750
|
-
const dragDropOk = await simulateDragDropUpload(target, fixture, logger);
|
|
4751
|
-
if (dragDropOk) {
|
|
4752
|
-
logger.info(`upload primitive: drag-drop fallback succeeded (name=${fixture.name}, size=${fixture.buffer.length}b)`);
|
|
4753
|
-
return true;
|
|
4754
|
-
}
|
|
4902
|
+
// A dispatched drop is unverified (no network signal, no .files), so it is
|
|
4903
|
+
// not success here: return false and let surfaceAndUpload run the verified
|
|
4904
|
+
// drop and the trusted-click/chooser routes.
|
|
4905
|
+
logger.warn("upload primitive: no network activity within timeout and no file attached in DOM; deferring to surface-and-upload");
|
|
4755
4906
|
return false;
|
|
4756
4907
|
}
|
|
4757
4908
|
logger.info(`upload primitive: file attached in DOM after setInputFiles (deferred-upload widget; name=${fixture.name}, filesLength=${attachedLength})`);
|
|
@@ -5408,7 +5559,7 @@ function pickSegmentedDateGroup(instruction, groups, value, compatible) {
|
|
|
5408
5559
|
// A group under another heading is not the field the step names, however
|
|
5409
5560
|
// well its label matches: the step's own words must find the section.
|
|
5410
5561
|
// A section that only repeats the group's own label is no heading at all
|
|
5411
|
-
// (
|
|
5562
|
+
// (a widget may enumerate "From*" as both), so it cannot name another section.
|
|
5412
5563
|
const namesOtherSection = (group) => {
|
|
5413
5564
|
const section = segmentedDateTokens(group.section ?? "");
|
|
5414
5565
|
const label = segmentedDateTokens(group.label ?? "");
|
|
@@ -5469,7 +5620,7 @@ function pickSegmentedDateGroup(instruction, groups, value, compatible) {
|
|
|
5469
5620
|
if (leaders.length > 1) {
|
|
5470
5621
|
// A nested group is one wrapper's slice of a larger date widget (e.g. the
|
|
5471
5622
|
// Year half of "From MM/YYYY"), so it only ties with a standalone group of
|
|
5472
|
-
// the same label when no section text separates them —
|
|
5623
|
+
// the same label when no section text separates them — a form may render
|
|
5473
5624
|
// "Work Experience 1 From" and "Education 1 From" with no heading the
|
|
5474
5625
|
// enumerator can see. The standalone group is the field the step names.
|
|
5475
5626
|
const standalone = leaders.filter((i) => !groups[i]?.nested);
|
|
@@ -5482,71 +5633,13 @@ function pickSegmentedDateGroup(instruction, groups, value, compatible) {
|
|
|
5482
5633
|
const only = groups.flatMap((g, i) => (compatible(g) && !g.nested ? [i] : []));
|
|
5483
5634
|
return only.length === 1 ? (only[0] ?? null) : null;
|
|
5484
5635
|
}
|
|
5485
|
-
/**
|
|
5486
|
-
* In-page helpers shared by the date-segment enumeration and the fill
|
|
5487
|
-
* readback so "is this spinbutton one segment of a multi-segment widget" has a
|
|
5488
|
-
* single definition; defines `spins`, `ownerOf` and `isDateSegment`.
|
|
5489
|
-
*/
|
|
5490
|
-
const SEGMENT_OWNER_JS = `
|
|
5491
|
-
const hiddenAttr = (node) => node.hasAttribute("hidden") || node.getAttribute("aria-hidden") === "true";
|
|
5492
|
-
const styleOf = (node) => {
|
|
5493
|
-
try { return typeof getComputedStyle === "function" ? getComputedStyle(node) : window.getComputedStyle(node); } catch { return null; }
|
|
5494
|
-
};
|
|
5495
|
-
const isHidden = (el) => {
|
|
5496
|
-
for (let node = el; node && node.nodeType === 1; node = node.parentElement) {
|
|
5497
|
-
if (hiddenAttr(node)) return true;
|
|
5498
|
-
const style = styleOf(node);
|
|
5499
|
-
if (style && (style.display === "none" || style.visibility === "hidden")) return true;
|
|
5500
|
-
}
|
|
5501
|
-
return false;
|
|
5502
|
-
};
|
|
5503
|
-
const spins = Array.from(document.querySelectorAll("input[role='spinbutton']")).filter((el) => !isHidden(el));
|
|
5504
|
-
const norm = (s) => (s || "").replace(/\\s+/g, " ").trim();
|
|
5505
|
-
const idText = (ids) => (ids || "").split(/\\s+/).map((id) => {
|
|
5506
|
-
const ref = id ? document.getElementById(id) : null;
|
|
5507
|
-
return ref ? norm(ref.textContent) : "";
|
|
5508
|
-
}).join(" ");
|
|
5509
|
-
const legendOf = (node) => {
|
|
5510
|
-
const legend = node.tagName === "FIELDSET" ? node.querySelector(":scope > legend") : null;
|
|
5511
|
-
return legend ? norm(legend.textContent) : "";
|
|
5512
|
-
};
|
|
5513
|
-
const namedBy = (node) => [legendOf(node), norm(node.getAttribute("aria-label")), idText(node.getAttribute("aria-labelledby"))].filter(Boolean).join(" ");
|
|
5514
|
-
const tiedToSpin = (l) => l.querySelector("input,[role='spinbutton']") || (l.getAttribute("for") && spins.some((sp) => sp.id === l.getAttribute("for")));
|
|
5515
|
-
const hasCaption = (node) => namedBy(node) !== "" || Array.from(node.querySelectorAll("label,legend")).some((l) => !tiedToSpin(l));
|
|
5516
|
-
const captionOwnerOf = (el) => {
|
|
5517
|
-
const explicit = el.closest("fieldset,[role='group']");
|
|
5518
|
-
if (explicit) return explicit;
|
|
5519
|
-
for (let node = el.parentElement; node && node !== document.body; node = node.parentElement) {
|
|
5520
|
-
if (hasCaption(node)) return node;
|
|
5521
|
-
}
|
|
5522
|
-
return null;
|
|
5523
|
-
};
|
|
5524
|
-
const bareSpins = spins.filter((sp) => !captionOwnerOf(sp));
|
|
5525
|
-
const hasOtherControl = (node) => Array.from(node.querySelectorAll("input,select,textarea")).some((c) => !spins.includes(c) && c.type !== "hidden");
|
|
5526
|
-
// Segments that each sit in their own uncaptioned wrapper share no
|
|
5527
|
-
// fieldset/caption, so the nearest ancestor holding several bare spinbuttons
|
|
5528
|
-
// (and no other form control) is the widget they belong to.
|
|
5529
|
-
const bareOwnerOf = (el) => {
|
|
5530
|
-
for (let node = el.parentElement; node && node !== document.body; node = node.parentElement) {
|
|
5531
|
-
if (hasOtherControl(node)) break;
|
|
5532
|
-
if (bareSpins.filter((sp) => node.contains(sp)).length > 1) return node;
|
|
5533
|
-
}
|
|
5534
|
-
return el.parentElement;
|
|
5535
|
-
};
|
|
5536
|
-
const ownerOf = (el) => captionOwnerOf(el) || bareOwnerOf(el);
|
|
5537
|
-
const isDateSegment = (el) => {
|
|
5538
|
-
if (!el.getAttribute || el.getAttribute("role") !== "spinbutton" || !spins.includes(el)) return false;
|
|
5539
|
-
const box = ownerOf(el);
|
|
5540
|
-
return spins.filter((sp) => ownerOf(sp) === box).length > 1;
|
|
5541
|
-
};
|
|
5542
|
-
`;
|
|
5543
5636
|
const SEGMENTED_DATE_MARKER_ATTR = "data-barnacle-segdate";
|
|
5544
5637
|
/** Exported so tests can run the real DOM enumeration against markup. */
|
|
5545
5638
|
exports.SEGMENTED_DATE_ENUMERATE_EXPR = `(() => {
|
|
5546
5639
|
const attr = ${JSON.stringify(SEGMENTED_DATE_MARKER_ATTR)};
|
|
5547
5640
|
const stale = document.querySelectorAll("[" + attr + "]");
|
|
5548
5641
|
stale.forEach((el) => el.removeAttribute(attr));
|
|
5549
|
-
${SEGMENT_OWNER_JS}
|
|
5642
|
+
${segmented_date_spinbutton_1.SEGMENT_OWNER_JS}
|
|
5550
5643
|
if (spins.length === 0) return { groups: [] };
|
|
5551
5644
|
const headingSibling = (node) => {
|
|
5552
5645
|
let prevSib = node.previousElementSibling;
|
|
@@ -5600,15 +5693,21 @@ exports.SEGMENTED_DATE_ENUMERATE_EXPR = `(() => {
|
|
|
5600
5693
|
const prior = seg.getAttribute(attr);
|
|
5601
5694
|
seg.setAttribute(attr, prior ? prior + " " + token : token);
|
|
5602
5695
|
};
|
|
5603
|
-
const addGroup = (segs, box, nested) => {
|
|
5696
|
+
const addGroup = (segs, box, nested, outer) => {
|
|
5604
5697
|
const gi = groups.length;
|
|
5605
5698
|
segs.forEach((seg, si) => mark(seg, gi + ":" + si));
|
|
5606
5699
|
let inner = null;
|
|
5607
|
-
|
|
5608
|
-
|
|
5609
|
-
|
|
5700
|
+
// A wrapper holding one segment is captioned by the nearest ancestor that
|
|
5701
|
+
// holds that segment and no other: a caption over several segments names
|
|
5702
|
+
// the whole date, never one slice of it.
|
|
5703
|
+
const top = outer || box;
|
|
5704
|
+
const soleOwner = (node) => !outer || spins.filter((sp) => node.contains(sp)).length === segs.length;
|
|
5705
|
+
for (let node = segs[0].parentElement; node && node !== top && top.contains(node) && !inner; node = node.parentElement) {
|
|
5706
|
+
if (segs.every((seg) => node.contains(seg)) && hasCaption(node) && soleOwner(node)) inner = node;
|
|
5707
|
+
}
|
|
5708
|
+
if (inner === box) inner = null;
|
|
5610
5709
|
const labelBox = inner || box;
|
|
5611
|
-
const own = namedBy(labelBox) || ownCaption(labelBox) || segs.map((seg) => idText(seg.getAttribute("aria-labelledby"))).filter(Boolean).join(" ");
|
|
5710
|
+
const own = namedBy(labelBox) || ownCaption(labelBox) || segs.map((seg) => idText(seg.getAttribute("aria-labelledby"))).filter(Boolean).join(" ") || Array.from(new Set(segs.map((seg) => ownKey(seg)).filter(Boolean))).join(" ");
|
|
5612
5711
|
const section = sectionOf(labelBox, Boolean(inner));
|
|
5613
5712
|
groups.push({
|
|
5614
5713
|
segmentMaxes: segs.map((seg, si) => maxOf(seg, si, segs.length)),
|
|
@@ -5631,7 +5730,7 @@ exports.SEGMENTED_DATE_ENUMERATE_EXPR = `(() => {
|
|
|
5631
5730
|
}
|
|
5632
5731
|
const wrap = seg.closest("label");
|
|
5633
5732
|
if (wrap) parts.push(norm(wrap.textContent));
|
|
5634
|
-
return keyOf(parts.filter(Boolean).join(" "));
|
|
5733
|
+
return keyOf(Array.from(new Set(parts.filter(Boolean))).join(" "));
|
|
5635
5734
|
};
|
|
5636
5735
|
// A container's legend names the whole section; the field a segment belongs
|
|
5637
5736
|
// to is the caption naming it, or the caption nearest before it with no other
|
|
@@ -5645,8 +5744,15 @@ exports.SEGMENTED_DATE_ENUMERATE_EXPR = `(() => {
|
|
|
5645
5744
|
if (el === boxLegend) return;
|
|
5646
5745
|
if (el.matches("label,legend")) {
|
|
5647
5746
|
const target = el.getAttribute("for");
|
|
5648
|
-
if (isHidden(el) || el.querySelector("input,select,textarea,[role='spinbutton']")
|
|
5649
|
-
|
|
5747
|
+
if (isHidden(el) || el.querySelector("input,select,textarea,[role='spinbutton']")) return;
|
|
5748
|
+
const text = keyOf(el.textContent);
|
|
5749
|
+
// A label tied to one of these segments names the field that segment
|
|
5750
|
+
// opens; its text is only a part word ("Month") when it names nothing.
|
|
5751
|
+
if (target && box.querySelector("[id='" + target + "']")) {
|
|
5752
|
+
if (own.has(box.querySelector("[id='" + target + "']")) && text) current = text;
|
|
5753
|
+
return;
|
|
5754
|
+
}
|
|
5755
|
+
current = text;
|
|
5650
5756
|
return;
|
|
5651
5757
|
}
|
|
5652
5758
|
if (!own.has(el)) {
|
|
@@ -5669,10 +5775,8 @@ exports.SEGMENTED_DATE_ENUMERATE_EXPR = `(() => {
|
|
|
5669
5775
|
}
|
|
5670
5776
|
// A shared fieldset can hold several independent dates (From and To year
|
|
5671
5777
|
// spinbuttons side by side); each wrapper is then its own candidate group.
|
|
5672
|
-
|
|
5673
|
-
|
|
5674
|
-
return;
|
|
5675
|
-
}
|
|
5778
|
+
const split = byParent.size > 1;
|
|
5779
|
+
if (split) byParent.forEach((inner, parent) => nestedCandidates.push([inner, parent, box]));
|
|
5676
5780
|
const keys = fieldKeys(segs, box);
|
|
5677
5781
|
const runs = [];
|
|
5678
5782
|
segs.forEach((seg, si) => {
|
|
@@ -5680,9 +5784,13 @@ exports.SEGMENTED_DATE_ENUMERATE_EXPR = `(() => {
|
|
|
5680
5784
|
if (last && last.key === keys[si]) last.segs.push(seg);
|
|
5681
5785
|
else runs.push({ key: keys[si], segs: [seg] });
|
|
5682
5786
|
});
|
|
5683
|
-
|
|
5787
|
+
// Segments that each sit in their own display wrapper still form one date
|
|
5788
|
+
// when a shared caption names them, so the caption run is a candidate too;
|
|
5789
|
+
// a run that is exactly one wrapper's segments is already that wrapper's group.
|
|
5790
|
+
const isWrapperGroup = (run) => split && byParent.get(run.segs[0].parentElement).length === run.segs.length && run.segs.every((seg) => seg.parentElement === run.segs[0].parentElement);
|
|
5791
|
+
runs.filter((run) => run.key && !isWrapperGroup(run)).forEach((run) => captionRuns.push({ run, box, outerIndex, whole: run.segs.length === segs.length }));
|
|
5684
5792
|
});
|
|
5685
|
-
nestedCandidates.forEach(([inner, parent]) => addGroup(inner, parent, true));
|
|
5793
|
+
nestedCandidates.forEach(([inner, parent, outer]) => addGroup(inner, parent, true, outer));
|
|
5686
5794
|
captionRuns.forEach(({ run, box, outerIndex, whole }) => {
|
|
5687
5795
|
const outer = groups[outerIndex];
|
|
5688
5796
|
const same = (a, b) => a.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim() === b.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
|
|
@@ -5711,21 +5819,38 @@ const SEGMENTED_DATE_CLEAR_EXPR = `(() => {
|
|
|
5711
5819
|
*
|
|
5712
5820
|
* Why this exists: such widgets keep each segment's value in component state
|
|
5713
5821
|
* driven by `keydown`/`beforeinput`; `fill`/`insertText` bypasses that and
|
|
5714
|
-
* garbles the segments. Returns a marker id on commit, or null on
|
|
5715
|
-
*
|
|
5716
|
-
*
|
|
5822
|
+
* garbles the segments. Returns a marker id on commit, or null on any
|
|
5823
|
+
* non-commit. Callers that must tell "not applicable" from "refused" use
|
|
5824
|
+
* `runSegmentedDateSpinbuttonPrimitive`.
|
|
5717
5825
|
*/
|
|
5718
5826
|
async function trySegmentedDateSpinbuttonPrimitive(params) {
|
|
5827
|
+
const outcome = await runSegmentedDateSpinbuttonPrimitive(params);
|
|
5828
|
+
return outcome.kind === "committed" ? outcome.targetId : null;
|
|
5829
|
+
}
|
|
5830
|
+
/**
|
|
5831
|
+
* Same as `trySegmentedDateSpinbuttonPrimitive` but distinguishes "this step
|
|
5832
|
+
* is not a segmented date here" (fall through to the cascade) from "a
|
|
5833
|
+
* width-compatible segment group exists and the primitive could not deliver"
|
|
5834
|
+
* (refused: the cascade would write the value via insertText and garble the
|
|
5835
|
+
* segments, so the caller must fail the step instead).
|
|
5836
|
+
*/
|
|
5837
|
+
async function runSegmentedDateSpinbuttonPrimitive(params) {
|
|
5719
5838
|
const { page, target, instruction, logger } = params;
|
|
5839
|
+
const notApplicable = { kind: "not-applicable" };
|
|
5840
|
+
const refuse = (reason) => {
|
|
5841
|
+
logger.warn(`segmented date primitive: refused: ${reason}`);
|
|
5842
|
+
return { kind: "refused", reason };
|
|
5843
|
+
};
|
|
5844
|
+
let groupChosen = false;
|
|
5720
5845
|
const whole = parseSegmentedDateValue(instruction);
|
|
5721
5846
|
const part = parseSegmentedPartStep(instruction);
|
|
5722
5847
|
if (!whole && !part)
|
|
5723
|
-
return
|
|
5848
|
+
return notApplicable;
|
|
5724
5849
|
const sendCDP = frameCdpSender(page, target);
|
|
5725
5850
|
try {
|
|
5726
5851
|
const enumerated = (await target.evaluate(exports.SEGMENTED_DATE_ENUMERATE_EXPR));
|
|
5727
5852
|
if (!enumerated?.groups?.length)
|
|
5728
|
-
return
|
|
5853
|
+
return notApplicable;
|
|
5729
5854
|
const wholeGi = whole ? resolveSegmentedDateGroup(instruction, enumerated.groups) : null;
|
|
5730
5855
|
const partStep = wholeGi === null ? part : null;
|
|
5731
5856
|
const value = (wholeGi !== null ? whole : part);
|
|
@@ -5741,10 +5866,18 @@ async function trySegmentedDateSpinbuttonPrimitive(params) {
|
|
|
5741
5866
|
? pickSegmentedDateGroup(instruction, enumerated.groups, partStep, partFits)
|
|
5742
5867
|
: null);
|
|
5743
5868
|
if (gi === null) {
|
|
5744
|
-
logger.info("segmented date primitive: no unambiguous group; falling through");
|
|
5745
5869
|
await target.evaluate(SEGMENTED_DATE_CLEAR_EXPR);
|
|
5746
|
-
|
|
5870
|
+
const widthCompatible = enumerated.groups.some((group) => (whole !== null &&
|
|
5871
|
+
group.segmentMaxes.reduce((sum, max) => sum + String(max).length, 0) ===
|
|
5872
|
+
whole.digits.length) ||
|
|
5873
|
+
(partStep !== null && partFits(group)));
|
|
5874
|
+
if (widthCompatible) {
|
|
5875
|
+
return refuse("a width-compatible date group exists but the target group is ambiguous");
|
|
5876
|
+
}
|
|
5877
|
+
logger.info("segmented date primitive: no compatible group; falling through");
|
|
5878
|
+
return notApplicable;
|
|
5747
5879
|
}
|
|
5880
|
+
groupChosen = true;
|
|
5748
5881
|
const maxes = enumerated.groups[gi]?.segmentMaxes ?? [];
|
|
5749
5882
|
const widths = maxes.map((max) => String(max).length);
|
|
5750
5883
|
const onlySegment = partStep ? partOf(enumerated.groups[gi]) : null;
|
|
@@ -5841,15 +5974,16 @@ async function trySegmentedDateSpinbuttonPrimitive(params) {
|
|
|
5841
5974
|
actual = await readSegments();
|
|
5842
5975
|
await target.evaluate(SEGMENTED_DATE_CLEAR_EXPR);
|
|
5843
5976
|
if (!matches(actual)) {
|
|
5844
|
-
|
|
5845
|
-
return null;
|
|
5977
|
+
return refuse(`readback ${maskDateDigits(JSON.stringify(actual))} != ${maskDateDigits(JSON.stringify(expected))}`);
|
|
5846
5978
|
}
|
|
5847
5979
|
logger.info(`segmented date primitive: committed segments ${maskDateDigits(JSON.stringify(actual))}`);
|
|
5848
|
-
return `segmented-date-${gi}
|
|
5980
|
+
return { kind: "committed", targetId: `segmented-date-${gi}` };
|
|
5849
5981
|
}
|
|
5850
5982
|
catch (err) {
|
|
5983
|
+
if (groupChosen)
|
|
5984
|
+
return refuse(`delivery threw: ${(0, errors_1.toErrorMessage)(err)}`);
|
|
5851
5985
|
logger.warn(`segmented date primitive: threw: ${(0, errors_1.toErrorMessage)(err)}; falling through`);
|
|
5852
|
-
return
|
|
5986
|
+
return notApplicable;
|
|
5853
5987
|
}
|
|
5854
5988
|
}
|
|
5855
5989
|
/**
|
|
@@ -6215,15 +6349,103 @@ function isNavigatingEvaluateRejection(error) {
|
|
|
6215
6349
|
message.includes("no frame for given id found") ||
|
|
6216
6350
|
GENERIC_CONTENT_FREE_EVAL_REJECTION_MESSAGES.has(message.trim()));
|
|
6217
6351
|
}
|
|
6352
|
+
const DEFAULT_POST_CLICK_SETTLE_IO = {
|
|
6353
|
+
settle: settleWithPageQuiescence,
|
|
6354
|
+
snapshot: snapshotPage,
|
|
6355
|
+
};
|
|
6356
|
+
/**
|
|
6357
|
+
* Re-settle rounds granted to a post-click page whose header flipped but whose
|
|
6358
|
+
* body has not re-rendered yet. Each round waits the settle's own idle
|
|
6359
|
+
* allowance, so the rounds together span the settle's MAX_MS ceiling rather
|
|
6360
|
+
* than a fixed guess; a held-still text ends the loop sooner.
|
|
6361
|
+
*/
|
|
6362
|
+
const HEADER_ONLY_RESETTLE_ROUNDS = Math.floor(click_settle_1.MAX_MS / click_settle_1.IDLE_GIVE_UP_MS);
|
|
6363
|
+
/**
|
|
6364
|
+
* Re-reads the post-click page while only its header has flipped. One
|
|
6365
|
+
* settle-then-snapshot moment can land between a header flip and the body
|
|
6366
|
+
* re-render that follows it, so the verdict waits for the body or for the text
|
|
6367
|
+
* to hold still through the whole re-settle budget, and only then reads the
|
|
6368
|
+
* header flip as the page's final state (`settled`): a single quiet round
|
|
6369
|
+
* cannot distinguish a finished page from a body that re-renders later. Each round settles against the
|
|
6370
|
+
* pre-click text, so the header flip stays movement and the quiet window, not the silent no-op
|
|
6371
|
+
* give-up, decides how long a body re-render is waited for. Bounded; a page whose body already swapped
|
|
6372
|
+
* returns at once, and one showing no change gets a single probe round so a
|
|
6373
|
+
* re-render that lands after the first snapshot is still seen.
|
|
6374
|
+
*/
|
|
6375
|
+
async function resettleThroughHeaderOnlyFlip(page, target, signalCounter, pre, post, io = DEFAULT_POST_CLICK_SETTLE_IO) {
|
|
6376
|
+
let current = post;
|
|
6377
|
+
for (let round = 0; round < HEADER_ONLY_RESETTLE_ROUNDS; round++) {
|
|
6378
|
+
const moved = (0, phantom_click_1.hasVisibleTextChanged)(pre, current);
|
|
6379
|
+
if (moved && (0, phantom_click_1.hasPageBodySwapped)(pre, current))
|
|
6380
|
+
return round > 0 ? { ...current, settled: true } : current;
|
|
6381
|
+
if (!moved) {
|
|
6382
|
+
// A click unseen to move anything gets one settle-free re-read, so a genuine
|
|
6383
|
+
// no-op adds no wait to its give-up bound.
|
|
6384
|
+
if (round > 0)
|
|
6385
|
+
return current;
|
|
6386
|
+
const probe = await io.snapshot(target, signalCounter, page);
|
|
6387
|
+
if (!(0, phantom_click_1.hasVisibleTextChanged)(pre, probe))
|
|
6388
|
+
return current;
|
|
6389
|
+
current = probe;
|
|
6390
|
+
continue;
|
|
6391
|
+
}
|
|
6392
|
+
await io.settle(page, target, signalCounter, current.networkCount, true, pre.visibleTextSignature, pre.visibleTextDigest);
|
|
6393
|
+
const next = await io.snapshot(target, signalCounter, page);
|
|
6394
|
+
const heldStill = !(0, phantom_click_1.hasVisibleTextChanged)(current, next);
|
|
6395
|
+
current = next;
|
|
6396
|
+
if (heldStill && !(0, phantom_click_1.hasPageBodySwapped)(pre, current))
|
|
6397
|
+
return { ...current, settled: true };
|
|
6398
|
+
}
|
|
6399
|
+
return current;
|
|
6400
|
+
}
|
|
6401
|
+
/**
|
|
6402
|
+
* Text baseline of the step-start page with the attempt's own network count, so
|
|
6403
|
+
* a late re-render an earlier attempt already absorbed into `pre` still reads as
|
|
6404
|
+
* movement to the settle while only this click's requests count as its own.
|
|
6405
|
+
*/
|
|
6406
|
+
function withNetworkBaseline(baseline, pre) {
|
|
6407
|
+
return { ...baseline, networkCount: pre.networkCount };
|
|
6408
|
+
}
|
|
6409
|
+
/**
|
|
6410
|
+
* Single owner of the settle -> snapshot -> header-only resettle sequence, so
|
|
6411
|
+
* every post-click site applies the same settled-ness rule. A click that cannot
|
|
6412
|
+
* trigger a round-trip is snapshotted as-is. The resettle does not depend on
|
|
6413
|
+
* `waitForMutations`, since an unflagged step can still advance a page whose
|
|
6414
|
+
* body re-renders late.
|
|
6415
|
+
*/
|
|
6416
|
+
async function settleSnapshotAndResettle(target, signalCounter, page, pre, mayTriggerRoundTrip, waitForMutations, io) {
|
|
6417
|
+
if (page && mayTriggerRoundTrip)
|
|
6418
|
+
await io.settle(page, target, signalCounter, pre.networkCount, waitForMutations, pre.visibleTextSignature, pre.visibleTextDigest);
|
|
6419
|
+
const post = await io.snapshot(target, signalCounter, page);
|
|
6420
|
+
return page && mayTriggerRoundTrip
|
|
6421
|
+
? resettleThroughHeaderOnlyFlip(page, target, signalCounter, pre, post, io)
|
|
6422
|
+
: post;
|
|
6423
|
+
}
|
|
6218
6424
|
/**
|
|
6219
6425
|
* Settles after a click and returns the post-click snapshot, so the settle
|
|
6220
6426
|
* wait and the phantom classifier decide from the same reader and baseline.
|
|
6221
6427
|
* Without a page there is nothing to wait on and the snapshot is taken as-is.
|
|
6428
|
+
* `io` lets a caller that owns its own seam (recon-browser's pre-replan
|
|
6429
|
+
* short-circuit) route the settle and the snapshot through the functions it
|
|
6430
|
+
* already imports, so the settled read there is the same one every other site
|
|
6431
|
+
* makes.
|
|
6432
|
+
*/
|
|
6433
|
+
async function settleAndSnapshotPostClick(target, signalCounter, page, pre, waitForMutations = true, io = DEFAULT_POST_CLICK_SETTLE_IO) {
|
|
6434
|
+
return settleSnapshotAndResettle(target, signalCounter, page, pre, true, waitForMutations, io);
|
|
6435
|
+
}
|
|
6436
|
+
/**
|
|
6437
|
+
* Single mid-flow post-click advance read: picks the advance baseline (the
|
|
6438
|
+
* step-start snapshot for a mid-flow submit, `pre` otherwise), gives it the
|
|
6439
|
+
* attempt's network count, settles, re-settles through a header-only flip and
|
|
6440
|
+
* judges advance against that same baseline, so every site credits a page that
|
|
6441
|
+
* advanced from step start once it is quiescent.
|
|
6222
6442
|
*/
|
|
6223
|
-
async function
|
|
6224
|
-
|
|
6225
|
-
|
|
6226
|
-
|
|
6443
|
+
async function readMidFlowPostClickAdvance(args) {
|
|
6444
|
+
const advanceBaseline = args.midFlowSubmit
|
|
6445
|
+
? withNetworkBaseline(args.stepStartSnapshot, args.pre)
|
|
6446
|
+
: args.pre;
|
|
6447
|
+
const post = await settleSnapshotAndResettle(args.target, args.signalCounter, args.page, advanceBaseline, args.mayTriggerRoundTrip ?? true, args.waitForMutations ?? true, args.io ?? DEFAULT_POST_CLICK_SETTLE_IO);
|
|
6448
|
+
return { post, advanceBaseline, pageAdvanced: (0, phantom_click_1.hasSettledPageAdvanced)(advanceBaseline, post) };
|
|
6227
6449
|
}
|
|
6228
6450
|
/**
|
|
6229
6451
|
* Shared in-page discovery of the widget's callback, used identically by
|
|
@@ -6451,6 +6673,38 @@ async function injectCaptchaTokenAndSubmit(target, token, responseField = "h-cap
|
|
|
6451
6673
|
await target.evaluate(dispatchChangeExpr).catch(() => undefined);
|
|
6452
6674
|
return { injected, hasForm, callbackDiscovered };
|
|
6453
6675
|
}
|
|
6676
|
+
/**
|
|
6677
|
+
* Single assembly point for the mid-flow half of a phantom-click verdict, so
|
|
6678
|
+
* the primary attempt and every bypass/retry site extend the same
|
|
6679
|
+
* pre-URL destination exemption for a mid-flow submit, granted only on a
|
|
6680
|
+
* settled page advance, identically instead of each re-deriving it.
|
|
6681
|
+
*/
|
|
6682
|
+
function midFlowDestinationPlausible(params) {
|
|
6683
|
+
const { step, post, midFlowSubmit } = params;
|
|
6684
|
+
const baseline = params.advanceBaseline;
|
|
6685
|
+
return ((params.destinationPlausible ?? (0, phantom_click_1.isPlausibleStepDestination)(step, post.url)) ||
|
|
6686
|
+
(midFlowSubmit &&
|
|
6687
|
+
(0, phantom_click_1.hasSettledPageAdvanced)(baseline, post) &&
|
|
6688
|
+
(0, phantom_click_1.isPlausibleStepDestination)(step, post.url, baseline.url)));
|
|
6689
|
+
}
|
|
6690
|
+
/**
|
|
6691
|
+
* Classifies a submit-shaped click through the shared mid-flow verdict
|
|
6692
|
+
* assembly used by the primary path and the n+16 / deep-submit / captcha /
|
|
6693
|
+
* child-frame bypass sites.
|
|
6694
|
+
*/
|
|
6695
|
+
function classifySubmitShapedClick(params) {
|
|
6696
|
+
const { pre, post, midFlowSubmit } = params;
|
|
6697
|
+
return (0, phantom_click_1.classifyPhantomClick)({
|
|
6698
|
+
actResultSuccess: params.actResultSuccess ?? true,
|
|
6699
|
+
pre,
|
|
6700
|
+
post,
|
|
6701
|
+
elementStateChanged: params.elementStateChanged,
|
|
6702
|
+
isSubmitShapedStep: midFlowSubmit || (params.isSubmitShapedStep ?? true),
|
|
6703
|
+
isMidFlowSubmitStep: midFlowSubmit,
|
|
6704
|
+
advanceBaseline: params.advanceBaseline,
|
|
6705
|
+
destinationPlausible: midFlowDestinationPlausible(params),
|
|
6706
|
+
});
|
|
6707
|
+
}
|
|
6454
6708
|
/**
|
|
6455
6709
|
* Explicit submit fallback for {@link injectCaptchaTokenAndSubmit} callers:
|
|
6456
6710
|
* used only when the caller's own transition poll observed no advance after
|
|
@@ -6522,6 +6776,8 @@ async function submitCaptchaGatedForm(target, responseField = "h-captcha-respons
|
|
|
6522
6776
|
const pre = verification
|
|
6523
6777
|
? await snapshotPage(target, verification.signalCounter, verification.page)
|
|
6524
6778
|
: null;
|
|
6779
|
+
if (pre)
|
|
6780
|
+
await armPageQuiescence(target);
|
|
6525
6781
|
const clickResult = await target
|
|
6526
6782
|
.evaluate((0, submit_control_1.buildClickByDeepIndexExpr)(top.deepIndex))
|
|
6527
6783
|
.catch((err) => {
|
|
@@ -6532,14 +6788,30 @@ async function submitCaptchaGatedForm(target, responseField = "h-captcha-respons
|
|
|
6532
6788
|
if (clickResult.clicked) {
|
|
6533
6789
|
if (!pre || !verification)
|
|
6534
6790
|
return true;
|
|
6535
|
-
|
|
6536
|
-
|
|
6537
|
-
|
|
6791
|
+
// An omitted flag means the caller could not tell final from mid-flow;
|
|
6792
|
+
// derive from the step like the in-cascade sites rather than going strict.
|
|
6793
|
+
const midFlowSubmit = verification.isMidFlowSubmitStep ??
|
|
6794
|
+
isMidFlowSubmitShapedStep({
|
|
6795
|
+
step: verification.step,
|
|
6796
|
+
submitStep: false,
|
|
6797
|
+
isFinalStep: false,
|
|
6798
|
+
flowHasSubmitSemantics: false,
|
|
6799
|
+
elementSubmitShaped: true,
|
|
6800
|
+
});
|
|
6801
|
+
const { post, advanceBaseline } = await readMidFlowPostClickAdvance({
|
|
6802
|
+
target,
|
|
6803
|
+
signalCounter: verification.signalCounter,
|
|
6804
|
+
page: verification.page,
|
|
6805
|
+
pre,
|
|
6806
|
+
stepStartSnapshot: verification.stepStartSnapshot ?? pre,
|
|
6807
|
+
midFlowSubmit,
|
|
6808
|
+
});
|
|
6809
|
+
const verdict = classifySubmitShapedClick({
|
|
6810
|
+
step: verification.step,
|
|
6538
6811
|
pre,
|
|
6539
6812
|
post,
|
|
6540
|
-
|
|
6541
|
-
|
|
6542
|
-
destinationPlausible: (0, phantom_click_1.isPlausibleStepDestination)(verification.step, post.url, verification.isMidFlowSubmitStep ? pre.url : undefined),
|
|
6813
|
+
midFlowSubmit,
|
|
6814
|
+
advanceBaseline,
|
|
6543
6815
|
});
|
|
6544
6816
|
if (verdict !== "phantom")
|
|
6545
6817
|
return true;
|
|
@@ -6553,14 +6825,20 @@ async function submitCaptchaGatedForm(target, responseField = "h-captcha-respons
|
|
|
6553
6825
|
throw err;
|
|
6554
6826
|
});
|
|
6555
6827
|
if (runnerUpClickResult.clicked) {
|
|
6556
|
-
const runnerUpPost = await
|
|
6557
|
-
|
|
6558
|
-
|
|
6828
|
+
const { post: runnerUpPost, advanceBaseline: runnerUpBaseline } = await readMidFlowPostClickAdvance({
|
|
6829
|
+
target,
|
|
6830
|
+
signalCounter: verification.signalCounter,
|
|
6831
|
+
page: verification.page,
|
|
6832
|
+
pre: post,
|
|
6833
|
+
stepStartSnapshot: verification.stepStartSnapshot ?? pre,
|
|
6834
|
+
midFlowSubmit,
|
|
6835
|
+
});
|
|
6836
|
+
const runnerUpVerdict = classifySubmitShapedClick({
|
|
6837
|
+
step: verification.step,
|
|
6559
6838
|
pre: post,
|
|
6560
6839
|
post: runnerUpPost,
|
|
6561
|
-
|
|
6562
|
-
|
|
6563
|
-
destinationPlausible: (0, phantom_click_1.isPlausibleStepDestination)(verification.step, runnerUpPost.url, verification.isMidFlowSubmitStep ? post.url : undefined),
|
|
6840
|
+
midFlowSubmit,
|
|
6841
|
+
advanceBaseline: runnerUpBaseline,
|
|
6564
6842
|
});
|
|
6565
6843
|
if (runnerUpVerdict !== "phantom")
|
|
6566
6844
|
return true;
|
|
@@ -8766,6 +9044,9 @@ async function hasUnfilledRequiredControlForStep(target, instruction) {
|
|
|
8766
9044
|
* upload/dropzone/file-related class or tag names. Tries multiple
|
|
8767
9045
|
* candidates in order. Returns true on first success.
|
|
8768
9046
|
*
|
|
9047
|
+
* Dispatch-only: a true return means the drop events fired, not that the
|
|
9048
|
+
* widget accepted the file; callers must verify.
|
|
9049
|
+
*
|
|
8769
9050
|
* Site-agnostic — DataTransfer + DragEvent dispatch is universal HTML5
|
|
8770
9051
|
* drag-and-drop API. Works on react-dropzone, Material Dropzone, custom
|
|
8771
9052
|
* <uapp-upload>/<app-upload>, and any other drop-zone-based upload UI.
|
|
@@ -9821,6 +10102,7 @@ function primitiveOptionLabel(option, questionLabel, redactValue, fromFill = fal
|
|
|
9821
10102
|
*/
|
|
9822
10103
|
async function probeChildFrameSubmitFallback(params) {
|
|
9823
10104
|
const { page, signalCounter, step, isMidFlowSubmitStep } = params;
|
|
10105
|
+
const midFlowSubmit = isMidFlowSubmitStep === true;
|
|
9824
10106
|
// `page.mainFrameId`/`page.frames` are read defensively: some call sites
|
|
9825
10107
|
// (and their test fakes) model a `Page` that never attaches child frames at
|
|
9826
10108
|
// all and doesn't implement this pair, which is indistinguishable here from
|
|
@@ -9870,21 +10152,27 @@ async function probeChildFrameSubmitFallback(params) {
|
|
|
9870
10152
|
// biome-ignore lint/style/noNonNullAssertion: guarded by the length check above
|
|
9871
10153
|
const top = ranked[0];
|
|
9872
10154
|
const pre = await snapshotPage(target, signalCounter, page);
|
|
10155
|
+
await armPageQuiescence(target);
|
|
9873
10156
|
const clickResult = (await Promise.resolve(target.evaluate((0, submit_control_1.buildClickByDeepIndexExpr)(top.deepIndex))).catch(() => ({ clicked: false })));
|
|
9874
10157
|
if (!clickResult.clicked)
|
|
9875
10158
|
continue;
|
|
9876
|
-
|
|
9877
|
-
|
|
9878
|
-
|
|
9879
|
-
|
|
9880
|
-
|
|
9881
|
-
|
|
9882
|
-
|
|
10159
|
+
// The step-start snapshot describes the resolved frame, not this child
|
|
10160
|
+
// frame, so the first click's own pre-click page is the baseline both
|
|
10161
|
+
// the top pick and the runner-up are judged against.
|
|
10162
|
+
const { post, advanceBaseline } = await readMidFlowPostClickAdvance({
|
|
10163
|
+
target,
|
|
10164
|
+
signalCounter,
|
|
10165
|
+
page,
|
|
10166
|
+
pre,
|
|
10167
|
+
stepStartSnapshot: pre,
|
|
10168
|
+
midFlowSubmit,
|
|
10169
|
+
});
|
|
10170
|
+
const verdict = classifySubmitShapedClick({
|
|
10171
|
+
step,
|
|
9883
10172
|
pre,
|
|
9884
10173
|
post,
|
|
9885
|
-
|
|
9886
|
-
|
|
9887
|
-
destinationPlausible: (0, phantom_click_1.isPlausibleStepDestination)(step, post.url, isMidFlowSubmitStep ? pre.url : undefined),
|
|
10174
|
+
midFlowSubmit,
|
|
10175
|
+
advanceBaseline,
|
|
9888
10176
|
});
|
|
9889
10177
|
if (verdict !== "phantom")
|
|
9890
10178
|
return target;
|
|
@@ -9894,18 +10182,20 @@ async function probeChildFrameSubmitFallback(params) {
|
|
|
9894
10182
|
const runnerUpClickResult = (await Promise.resolve(target.evaluate((0, submit_control_1.buildClickByDeepIndexExpr)(runnerUp.deepIndex))).catch(() => ({ clicked: false })));
|
|
9895
10183
|
if (!runnerUpClickResult.clicked)
|
|
9896
10184
|
continue;
|
|
9897
|
-
|
|
9898
|
-
|
|
9899
|
-
|
|
9900
|
-
|
|
9901
|
-
|
|
9902
|
-
|
|
9903
|
-
|
|
9904
|
-
|
|
10185
|
+
const { post: runnerUpPost } = await readMidFlowPostClickAdvance({
|
|
10186
|
+
target,
|
|
10187
|
+
signalCounter,
|
|
10188
|
+
page,
|
|
10189
|
+
pre: midFlowSubmit ? post : advanceBaseline,
|
|
10190
|
+
stepStartSnapshot: advanceBaseline,
|
|
10191
|
+
midFlowSubmit,
|
|
10192
|
+
});
|
|
10193
|
+
const runnerUpVerdict = classifySubmitShapedClick({
|
|
10194
|
+
step,
|
|
10195
|
+
pre: post,
|
|
9905
10196
|
post: runnerUpPost,
|
|
9906
|
-
|
|
9907
|
-
|
|
9908
|
-
destinationPlausible: (0, phantom_click_1.isPlausibleStepDestination)(step, runnerUpPost.url, isMidFlowSubmitStep ? post.url : undefined),
|
|
10197
|
+
midFlowSubmit,
|
|
10198
|
+
advanceBaseline,
|
|
9909
10199
|
});
|
|
9910
10200
|
if (runnerUpVerdict !== "phantom")
|
|
9911
10201
|
return target;
|
|
@@ -9939,7 +10229,14 @@ async function raceAbortSignal(work, signal) {
|
|
|
9939
10229
|
}
|
|
9940
10230
|
}
|
|
9941
10231
|
async function executeStepWithHealing(params) {
|
|
9942
|
-
const { stagehand, optional, upload, submitStep, captchaGated = false, emailStep = false, emailStepConfig, allocatedInbox = null, sessionProxy = null, flowHasSubmitSemantics:
|
|
10232
|
+
const { stagehand, optional, upload, submitStep, captchaGated = false, emailStep = false, emailStepConfig, allocatedInbox = null, sessionProxy = null, flowHasSubmitSemantics: flowHasSubmitSemanticsParam, stepIndex, totalSteps, phase, signalCounter, recentCaptures, recentCaptureMeta, anthropic, rephraseModel, logger, captureFn, uploadFixture, isFinalStep, submitEndpointPattern, submittedStateSelectors, requireSubmitEndpointMatch, advanceTransitionBodyPattern, successUrlFragments, successPageTitleHints, ownBackendHostnames, knownErrorClassPrefixes, wizardExitButtonLabels, getSuppressedAisdkElementIdErrorCount, trajectory, onStepFailure, onStepHeal, onElementStepStartBaseline, } = params;
|
|
10233
|
+
// An explicitly flagged final step is the flow's submit by definition (see
|
|
10234
|
+
// flowHasSubmitSemantics). An omitted flow-level flag is derived from the same
|
|
10235
|
+
// evidence flowHasSubmitSemantics reads (a submit flag, an endpoint pattern or
|
|
10236
|
+
// requirement), so a last slot with none of them is mid-flow rather than a
|
|
10237
|
+
// final submit held to network/URL proof.
|
|
10238
|
+
const flowSubmitEvidence = submitStep || submitEndpointPattern !== null || requireSubmitEndpointMatch;
|
|
10239
|
+
const flowHasSubmitSemanticsFlag = (flowHasSubmitSemanticsParam ?? flowSubmitEvidence) || (isFinalStep && submitStep);
|
|
9943
10240
|
// Mutable: the `emailStep` code-extract path splices the extracted code
|
|
9944
10241
|
// into this instruction (see the emailStep hook block below) so the
|
|
9945
10242
|
// pre-existing fill cascade downstream picks it up as the value to type
|
|
@@ -10119,7 +10416,11 @@ async function executeStepWithHealing(params) {
|
|
|
10119
10416
|
const stepDeclaredNonSubmit = isStepDeclaredNonSubmit({ step, submitStep });
|
|
10120
10417
|
// Single final-step inference every submit-strictness gate below reads, so a
|
|
10121
10418
|
// declared non-submit veto cannot be honoured at one site and missed at another.
|
|
10122
|
-
|
|
10419
|
+
// Finality goes through isGenuineFinalSubmit like the judge routing and
|
|
10420
|
+
// isMidFlowSubmitShapedStep: a last slot without submit semantics is mid-flow
|
|
10421
|
+
// and must not be held to final-submit proof here.
|
|
10422
|
+
const inferredFinalSubmitStep = isGenuineFinalSubmit({ isFinalStep, flowHasSubmitSemantics: flowHasSubmitSemanticsFlag }) &&
|
|
10423
|
+
!stepDeclaredNonSubmit;
|
|
10123
10424
|
const requireSubmitEndpoint = (inferredFinalSubmitStep || submitStep) && submitEndpointPattern !== null;
|
|
10124
10425
|
// Capture the per-element selection baseline (for verifyDomEffect's element-
|
|
10125
10426
|
// scoped click read-back) ONLY for selection/field-answer click steps — never
|
|
@@ -10257,15 +10558,18 @@ async function executeStepWithHealing(params) {
|
|
|
10257
10558
|
}
|
|
10258
10559
|
// Multi-segment date spinbuttons need real key events; the cascade's
|
|
10259
10560
|
// insertText would garble the segments. Falls through on null.
|
|
10260
|
-
const
|
|
10561
|
+
const segmentedDate = await runSegmentedDateSpinbuttonPrimitive({
|
|
10261
10562
|
page,
|
|
10262
10563
|
target: selectFrameTarget,
|
|
10263
10564
|
instruction: step,
|
|
10264
10565
|
logger,
|
|
10265
10566
|
});
|
|
10266
|
-
if (
|
|
10567
|
+
if (segmentedDate.kind === "refused") {
|
|
10568
|
+
throw new errors_2.StepVerificationError(`${formatStepPrefix(stepIndex, totalSteps)} (${(0, pii_redaction_1.redactPii)(step).slice(0, 60)}) segmented date could not be delivered: ${segmentedDate.reason}`, "cascade-exhausted");
|
|
10569
|
+
}
|
|
10570
|
+
if (segmentedDate.kind === "committed") {
|
|
10267
10571
|
logger.info(`${formatStepPrefix(stepIndex, totalSteps)} resolved by segmented-date primitive`);
|
|
10268
|
-
trajectory?.push({ stepIndex, verifiedBy: "dom", targetId:
|
|
10572
|
+
trajectory?.push({ stepIndex, verifiedBy: "dom", targetId: segmentedDate.targetId });
|
|
10269
10573
|
return "completed";
|
|
10270
10574
|
}
|
|
10271
10575
|
// On a CATCH-ALL step ("for any remaining … question"), fill every
|
|
@@ -10306,6 +10610,9 @@ async function executeStepWithHealing(params) {
|
|
|
10306
10610
|
const isSettleWaitStep = isSubmitOrFinalStep || isAdvanceStep(step);
|
|
10307
10611
|
if (captchaGated && isSubmitOrFinalStep) {
|
|
10308
10612
|
let captchaTarget = frameTarget ?? (0, frame_target_1.mainFrameTarget)(page);
|
|
10613
|
+
// The step-level baseline is declared after this block, so capture the
|
|
10614
|
+
// step-start page here for the fallback submit to judge against.
|
|
10615
|
+
const captchaStepStartSnapshot = await snapshotPage(captchaTarget, signalCounter, page);
|
|
10309
10616
|
const sitekeyProbeExpr = `(() => {
|
|
10310
10617
|
const el = document.querySelector("[data-sitekey]");
|
|
10311
10618
|
if (!el) return { siteKey: null, isInvisible: false };
|
|
@@ -10447,11 +10754,13 @@ async function executeStepWithHealing(params) {
|
|
|
10447
10754
|
signalCounter,
|
|
10448
10755
|
page,
|
|
10449
10756
|
step,
|
|
10757
|
+
stepStartSnapshot: captchaStepStartSnapshot ?? undefined,
|
|
10450
10758
|
isMidFlowSubmitStep: isMidFlowSubmitShapedStep({
|
|
10451
10759
|
step,
|
|
10452
10760
|
submitStep,
|
|
10453
10761
|
isFinalStep,
|
|
10454
10762
|
flowHasSubmitSemantics: flowHasSubmitSemanticsFlag,
|
|
10763
|
+
elementSubmitShaped: !stepDeclaredNonSubmit,
|
|
10455
10764
|
}),
|
|
10456
10765
|
});
|
|
10457
10766
|
}
|
|
@@ -10593,6 +10902,66 @@ async function executeStepWithHealing(params) {
|
|
|
10593
10902
|
// legitimate-transition check (below) scans captures landed during THIS
|
|
10594
10903
|
// step's processing — earlier-step transitions don't count.
|
|
10595
10904
|
const stepStartMetaLength = recentCaptureMeta.length;
|
|
10905
|
+
let firstAttemptPre = null;
|
|
10906
|
+
let stepStartSnapshot = isStepSubmitShaped({
|
|
10907
|
+
step,
|
|
10908
|
+
submitStep,
|
|
10909
|
+
isFinalStep,
|
|
10910
|
+
flowHasSubmitSemantics: flowHasSubmitSemanticsFlag,
|
|
10911
|
+
})
|
|
10912
|
+
? await snapshotPage(frameTarget ?? (0, frame_target_1.mainFrameTarget)(page), signalCounter, page)
|
|
10913
|
+
: null;
|
|
10914
|
+
// A click on a disabled control can still grow the DOM (a validation
|
|
10915
|
+
// re-render), which would read as a step-start advance — so the last
|
|
10916
|
+
// resolved click's disabled veto must also gate the baseline credit.
|
|
10917
|
+
let lastClickBlockedByDisabled = false;
|
|
10918
|
+
// A step whose submit shape is only known from a resolved element got no
|
|
10919
|
+
// step-level baseline; the first attempt's pre-click snapshot is the
|
|
10920
|
+
// step-start page, so every later verdict site judges against it.
|
|
10921
|
+
const adoptElementStepStartBaseline = () => {
|
|
10922
|
+
if (stepStartSnapshot || !firstAttemptPre)
|
|
10923
|
+
return;
|
|
10924
|
+
stepStartSnapshot = firstAttemptPre;
|
|
10925
|
+
onElementStepStartBaseline?.(firstAttemptPre);
|
|
10926
|
+
};
|
|
10927
|
+
// One derivation for every in-cascade verdict site, so the same click is
|
|
10928
|
+
// judged under the same strictness class and baseline whichever site sees it.
|
|
10929
|
+
const deriveMidFlowJudgement = (elementSubmitShaped, pre) => {
|
|
10930
|
+
const midFlowSubmit = isMidFlowSubmitShapedStep({
|
|
10931
|
+
step,
|
|
10932
|
+
submitStep,
|
|
10933
|
+
isFinalStep,
|
|
10934
|
+
flowHasSubmitSemantics: flowHasSubmitSemanticsFlag,
|
|
10935
|
+
elementSubmitShaped,
|
|
10936
|
+
});
|
|
10937
|
+
if (elementSubmitShaped)
|
|
10938
|
+
adoptElementStepStartBaseline();
|
|
10939
|
+
return { midFlowSubmit, advanceBaseline: stepStartSnapshot ?? pre };
|
|
10940
|
+
};
|
|
10941
|
+
const creditStepStartAdvance = async (exit) => {
|
|
10942
|
+
const genuineFinal = isGenuineFinalSubmit({
|
|
10943
|
+
isFinalStep,
|
|
10944
|
+
flowHasSubmitSemantics: flowHasSubmitSemanticsFlag,
|
|
10945
|
+
});
|
|
10946
|
+
if (!stepStartSnapshot || genuineFinal || lastClickBlockedByDisabled)
|
|
10947
|
+
return false;
|
|
10948
|
+
const { post: settled, pageAdvanced } = await readMidFlowPostClickAdvance({
|
|
10949
|
+
target: frameTarget ?? (0, frame_target_1.mainFrameTarget)(page),
|
|
10950
|
+
signalCounter,
|
|
10951
|
+
page,
|
|
10952
|
+
pre: stepStartSnapshot,
|
|
10953
|
+
stepStartSnapshot,
|
|
10954
|
+
midFlowSubmit: true,
|
|
10955
|
+
});
|
|
10956
|
+
if (!pageAdvanced)
|
|
10957
|
+
return false;
|
|
10958
|
+
// A DOM change that came from bouncing to an auth gate is not an advance.
|
|
10959
|
+
if (!(0, phantom_click_1.isPlausibleStepDestination)(step, settled.url, stepStartSnapshot.url))
|
|
10960
|
+
return false;
|
|
10961
|
+
logger.info(`${formatStepPrefix(stepIndex, totalSteps)} ${exit}: settled page advanced from the step-start baseline; crediting the step`);
|
|
10962
|
+
trajectory?.push({ stepIndex, verifiedBy: "dom" });
|
|
10963
|
+
return true;
|
|
10964
|
+
};
|
|
10596
10965
|
// Page-state probe BEFORE the cascade. When the page doesn't have any
|
|
10597
10966
|
// candidate matching the step's instruction we either skip cleanly
|
|
10598
10967
|
// (optional) or escalate straight to replan (required) — far cheaper than
|
|
@@ -10683,6 +11052,7 @@ async function executeStepWithHealing(params) {
|
|
|
10683
11052
|
submitStep,
|
|
10684
11053
|
isFinalStep,
|
|
10685
11054
|
flowHasSubmitSemantics: flowHasSubmitSemanticsFlag,
|
|
11055
|
+
elementSubmitShaped: !stepDeclaredNonSubmit,
|
|
10686
11056
|
}),
|
|
10687
11057
|
});
|
|
10688
11058
|
if (fallbackTarget) {
|
|
@@ -10691,6 +11061,8 @@ async function executeStepWithHealing(params) {
|
|
|
10691
11061
|
return "completed";
|
|
10692
11062
|
}
|
|
10693
11063
|
}
|
|
11064
|
+
if (await creditStepStartAdvance("probe-absent"))
|
|
11065
|
+
return "completed";
|
|
10694
11066
|
// Capture diagnostics + write a failure dump BEFORE throwing so the
|
|
10695
11067
|
// global replan path's `readFailureDumpEvidence` can populate the
|
|
10696
11068
|
// prompt's CURRENTLY VISIBLE / UNFOCUSED OBSERVE / PAGE BODY HTML
|
|
@@ -11021,6 +11393,21 @@ async function executeStepWithHealing(params) {
|
|
|
11021
11393
|
};
|
|
11022
11394
|
let phantomClickAfterAttempt1 = false;
|
|
11023
11395
|
let attempt1UnreachableViaLightDom = false;
|
|
11396
|
+
// Attempt 1 resolved no candidate at all and was skipped before any verdict,
|
|
11397
|
+
// so it never reached the "unresolved" escalation a judged attempt 1 takes.
|
|
11398
|
+
let attempt1FastSkipped = false;
|
|
11399
|
+
// The deep submit-control locator takes attempt 2 when attempt 1 judged its
|
|
11400
|
+
// target unreachable, and attempt 3 when attempt 1 was fast-skipped and the
|
|
11401
|
+
// observe-act retry in attempt 2 did not verify, so a submit-shaped step with
|
|
11402
|
+
// no resolvable light-DOM target is never left without it.
|
|
11403
|
+
const deepSubmitLocatorSlot = (attemptNumber) => isStepSubmitShaped({
|
|
11404
|
+
step,
|
|
11405
|
+
submitStep,
|
|
11406
|
+
isFinalStep,
|
|
11407
|
+
flowHasSubmitSemantics: flowHasSubmitSemanticsFlag,
|
|
11408
|
+
}) &&
|
|
11409
|
+
((attemptNumber === 2 && attempt1UnreachableViaLightDom) ||
|
|
11410
|
+
(attemptNumber === 3 && attempt1FastSkipped));
|
|
11024
11411
|
// Last candidate list the deep-submit-locator itself ranked this step, if
|
|
11025
11412
|
// any — reused by the cascade-exhausted diagnostic snapshot below so it
|
|
11026
11413
|
// doesn't re-run buildRankSubmitCandidatesExpr (and inflate the rank-call
|
|
@@ -11035,23 +11422,17 @@ async function executeStepWithHealing(params) {
|
|
|
11035
11422
|
// Skip to the next iteration so the cascade reaches its higher-value
|
|
11036
11423
|
// techniques faster.
|
|
11037
11424
|
if (attempt > 1) {
|
|
11038
|
-
const wouldBeTechnique = attempt
|
|
11039
|
-
?
|
|
11040
|
-
|
|
11041
|
-
|
|
11042
|
-
submitStep,
|
|
11043
|
-
isFinalStep,
|
|
11044
|
-
flowHasSubmitSemantics: flowHasSubmitSemanticsFlag,
|
|
11045
|
-
})
|
|
11046
|
-
? "deep-submit-locator"
|
|
11047
|
-
: phantomClickAfterAttempt1
|
|
11425
|
+
const wouldBeTechnique = deepSubmitLocatorSlot(attempt)
|
|
11426
|
+
? "deep-submit-locator"
|
|
11427
|
+
: attempt === 2
|
|
11428
|
+
? phantomClickAfterAttempt1
|
|
11048
11429
|
? "trusted-click-retry"
|
|
11049
11430
|
: "observe-act"
|
|
11050
|
-
|
|
11051
|
-
|
|
11052
|
-
|
|
11053
|
-
|
|
11054
|
-
|
|
11431
|
+
: attempt === 3
|
|
11432
|
+
? "structured-click"
|
|
11433
|
+
: attempt === 4
|
|
11434
|
+
? "observe-act-exclude"
|
|
11435
|
+
: "llm-rephrase";
|
|
11055
11436
|
const decision = shouldSkipTechnique({
|
|
11056
11437
|
technique: wouldBeTechnique,
|
|
11057
11438
|
priorAttempts: attempts.map((a) => ({
|
|
@@ -11081,7 +11462,15 @@ async function executeStepWithHealing(params) {
|
|
|
11081
11462
|
// A click from an earlier attempt that errored after opening a tab leaves the
|
|
11082
11463
|
// run on the stale page; pick the tab up before measuring anything.
|
|
11083
11464
|
await adoptOpenedPage(0);
|
|
11465
|
+
// A prior attempt's click may have advanced the page after its own verdict;
|
|
11466
|
+
// judge against the step-start baseline before `pre` absorbs the advance.
|
|
11467
|
+
if (attempt > 1 && (await creditStepStartAdvance(`attempt-${attempt}-entry`)))
|
|
11468
|
+
return "completed";
|
|
11084
11469
|
const pre = await snapshotPage(frameTarget ?? (0, frame_target_1.mainFrameTarget)(page), signalCounter, page, captureSelectionState);
|
|
11470
|
+
// Recorded before any fast-skip `continue`, so an attempt-1 act that threw
|
|
11471
|
+
// after its click landed still leaves the step-start page behind.
|
|
11472
|
+
firstAttemptPre ??= pre;
|
|
11473
|
+
await armPageQuiescence(frameTarget ?? (0, frame_target_1.mainFrameTarget)(page));
|
|
11085
11474
|
// Snapshot the meta-tail length so the final-step pattern gate can scope
|
|
11086
11475
|
// its URL scan to captures added DURING this attempt (not historical
|
|
11087
11476
|
// tail from earlier steps).
|
|
@@ -11116,6 +11505,15 @@ async function executeStepWithHealing(params) {
|
|
|
11116
11505
|
// whether this attempt's signal should come from the network/URL pair or
|
|
11117
11506
|
// from DOM re-read. Captured here so both branches of the cascade write to it.
|
|
11118
11507
|
let resolvedAction = null;
|
|
11508
|
+
// Submit shape of the control observe resolved BEFORE the act clicked it.
|
|
11509
|
+
// The act performs the click itself, so a page that swaps at once has
|
|
11510
|
+
// already removed the control by the time the post-act read runs; this is
|
|
11511
|
+
// the only read that cannot be blinded by the transition it caused.
|
|
11512
|
+
const submitShapeObservedBeforeClick = new Map();
|
|
11513
|
+
// Every submit-shaped control on the page before the act, so the shape of
|
|
11514
|
+
// the control the act clicked is known even when observe resolved nothing
|
|
11515
|
+
// or a different node than the act did.
|
|
11516
|
+
const submitShapedControlsBeforeClick = new Set();
|
|
11119
11517
|
// Stagehand resolves a natural-language act target internally, so for a
|
|
11120
11518
|
// click-intent instruction resolve it with observe first and centre it;
|
|
11121
11519
|
// a sticky bar otherwise swallows Stagehand's own click. Throws when the
|
|
@@ -11124,10 +11522,17 @@ async function executeStepWithHealing(params) {
|
|
|
11124
11522
|
const preCentreNaturalLanguageClick = async (instruction) => {
|
|
11125
11523
|
if (!(0, stagehand_guard_1.hasClickIntent)(instruction))
|
|
11126
11524
|
return false;
|
|
11525
|
+
if (!stepDeclaredNonSubmit) {
|
|
11526
|
+
for (const xpath of await readSubmitShapedControlXpaths(frameTarget ?? (0, frame_target_1.mainFrameTarget)(page))) {
|
|
11527
|
+
submitShapedControlsBeforeClick.add(xpath);
|
|
11528
|
+
}
|
|
11529
|
+
}
|
|
11127
11530
|
const observed = await (0, stagehand_guard_1.guardedObserve)(stagehand, instruction, { timeout: exports.STEP_WATCHDOG_MS }, captureFn, frameTarget);
|
|
11128
11531
|
const first = observed[0];
|
|
11129
11532
|
if (!(0, stagehand_guard_1.isTrustedClickMethod)(first?.method) || !first?.selector)
|
|
11130
11533
|
return true;
|
|
11534
|
+
submitShapeObservedBeforeClick.set(first.selector, !stepDeclaredNonSubmit &&
|
|
11535
|
+
(await resolvedClickTargetIsSubmitShaped(frameTarget ?? (0, frame_target_1.mainFrameTarget)(page), first.selector)));
|
|
11131
11536
|
const failure = await centreObservedClickTarget(frameTarget ?? (0, frame_target_1.mainFrameTarget)(page), first.selector);
|
|
11132
11537
|
if (failure)
|
|
11133
11538
|
throw new Error(failure);
|
|
@@ -11225,14 +11630,7 @@ async function executeStepWithHealing(params) {
|
|
|
11225
11630
|
}
|
|
11226
11631
|
}
|
|
11227
11632
|
}
|
|
11228
|
-
else if (attempt
|
|
11229
|
-
attempt1UnreachableViaLightDom &&
|
|
11230
|
-
isStepSubmitShaped({
|
|
11231
|
-
step,
|
|
11232
|
-
submitStep,
|
|
11233
|
-
isFinalStep,
|
|
11234
|
-
flowHasSubmitSemantics: flowHasSubmitSemanticsFlag,
|
|
11235
|
-
})) {
|
|
11633
|
+
else if (deepSubmitLocatorSlot(attempt)) {
|
|
11236
11634
|
// Deep submit-control locator: attempt 1 either phantom-clicked
|
|
11237
11635
|
// (Stagehand reported success but pre/post showed zero effect) or
|
|
11238
11636
|
// came back "unresolved" (act() found zero candidates at all), so the
|
|
@@ -11308,36 +11706,26 @@ async function executeStepWithHealing(params) {
|
|
|
11308
11706
|
const runnerUp = ranked[1];
|
|
11309
11707
|
if (runnerUp) {
|
|
11310
11708
|
const midTarget = frameTarget ?? (0, frame_target_1.mainFrameTarget)(page);
|
|
11311
|
-
|
|
11709
|
+
// Ranked candidates are submit-shaped by construction, so the
|
|
11710
|
+
// resolved element's shape is the step's declared-non-submit gate.
|
|
11711
|
+
const { midFlowSubmit: deepMidFlowSubmit, advanceBaseline: deepStepBaseline } = deriveMidFlowJudgement(!stepDeclaredNonSubmit, pre);
|
|
11712
|
+
const { post: midPost, advanceBaseline: deepAdvanceBaseline } = await readMidFlowPostClickAdvance({
|
|
11713
|
+
target: midTarget,
|
|
11714
|
+
signalCounter,
|
|
11312
11715
|
page,
|
|
11313
|
-
counter: signalCounter,
|
|
11314
11716
|
pre,
|
|
11315
|
-
|
|
11316
|
-
|
|
11317
|
-
readQuiescence: async () => midTarget.evaluate(click_settle_1.QUIESCENCE_PROBE_EXPR),
|
|
11717
|
+
stepStartSnapshot: deepStepBaseline,
|
|
11718
|
+
midFlowSubmit: deepMidFlowSubmit,
|
|
11318
11719
|
});
|
|
11319
|
-
|
|
11320
|
-
|
|
11720
|
+
// The deep-submit-locator runs only on submit escalation, so a
|
|
11721
|
+
// stray selection flip must not mask a top-pick phantom and skip
|
|
11722
|
+
// the runner-up retry.
|
|
11723
|
+
const topVerdict = classifySubmitShapedClick({
|
|
11724
|
+
step,
|
|
11321
11725
|
pre,
|
|
11322
11726
|
post: midPost,
|
|
11323
|
-
|
|
11324
|
-
|
|
11325
|
-
// the runner-up retry.
|
|
11326
|
-
isSubmitShapedStep: true,
|
|
11327
|
-
isMidFlowSubmitStep: isMidFlowSubmitShapedStep({
|
|
11328
|
-
step,
|
|
11329
|
-
submitStep,
|
|
11330
|
-
isFinalStep,
|
|
11331
|
-
flowHasSubmitSemantics: flowHasSubmitSemanticsFlag,
|
|
11332
|
-
}),
|
|
11333
|
-
destinationPlausible: (0, phantom_click_1.isPlausibleStepDestination)(step, midPost.url, isMidFlowSubmitShapedStep({
|
|
11334
|
-
step,
|
|
11335
|
-
submitStep,
|
|
11336
|
-
isFinalStep,
|
|
11337
|
-
flowHasSubmitSemantics: flowHasSubmitSemanticsFlag,
|
|
11338
|
-
})
|
|
11339
|
-
? pre.url
|
|
11340
|
-
: undefined),
|
|
11727
|
+
midFlowSubmit: deepMidFlowSubmit,
|
|
11728
|
+
advanceBaseline: deepAdvanceBaseline,
|
|
11341
11729
|
});
|
|
11342
11730
|
if (topVerdict === "phantom") {
|
|
11343
11731
|
logger.info(`${formatStepPrefix(stepIndex, totalSteps)} deep-submit-locator top pick (${top.tag} "${top.accessibleName}") phantom-clicked; retrying runner-up ${runnerUp.tag} "${runnerUp.accessibleName}" (tier ${runnerUp.tier})`);
|
|
@@ -11725,6 +12113,14 @@ async function executeStepWithHealing(params) {
|
|
|
11725
12113
|
};
|
|
11726
12114
|
}
|
|
11727
12115
|
}
|
|
12116
|
+
if (overriddenTarget.method === "fill" &&
|
|
12117
|
+
(await isDateSegmentAtSelector(frameTarget ?? (0, frame_target_1.mainFrameTarget)(page), overriddenTarget.selector))) {
|
|
12118
|
+
record.errorMessage = `refused fill on a date segment: "${target.description.slice(0, 60)}" (a lone-segment write cannot compose a date)`;
|
|
12119
|
+
attempts.push(record);
|
|
12120
|
+
failureReasons.push(record.errorMessage);
|
|
12121
|
+
logger.info(`${formatStepPrefix(stepIndex, totalSteps)} attempt ${attempt}: ${record.errorMessage}`);
|
|
12122
|
+
continue;
|
|
12123
|
+
}
|
|
11728
12124
|
params.abortSignal?.throwIfAborted();
|
|
11729
12125
|
// Stagehand's internal Locator.click() does not scroll past sticky
|
|
11730
12126
|
// bars, so centre + hit-test the observed control first; an
|
|
@@ -12064,39 +12460,67 @@ async function executeStepWithHealing(params) {
|
|
|
12064
12460
|
? `attempt 1 fast-skipped (no resolved action): ${record.errorMessage}`
|
|
12065
12461
|
: "attempt 1 fast-skipped (no resolved action; Stagehand returned no candidates)";
|
|
12066
12462
|
failureReasons.push(reason);
|
|
12463
|
+
attempt1FastSkipped = true;
|
|
12067
12464
|
continue;
|
|
12068
12465
|
}
|
|
12069
12466
|
// A fill/select/type action cannot open a tab, so skip the discovery wait for it.
|
|
12070
12467
|
await adoptOpenedPage(resolvedAction && STATE_CLASS_METHODS.has(resolvedAction.method ?? "") ? 0 : undefined);
|
|
12468
|
+
const isClick = resolvedAction !== null && resolvedAction.method === "click";
|
|
12469
|
+
// Attribute-based submit-shape read of the resolved click target itself —
|
|
12470
|
+
// independent of the flow-authored `submitStep` flag. Read before the
|
|
12471
|
+
// settle: a view swap removes the clicked control, so a post-swap read
|
|
12472
|
+
// would blind the mid-flow exemption on exactly the case it exists for.
|
|
12473
|
+
// See resolvedClickTargetIsSubmitShaped's doc comment.
|
|
12474
|
+
// A successful act that listed no actions still clicked what observe
|
|
12475
|
+
// resolved first, so that control's pre-click shape stands in for it.
|
|
12476
|
+
const resolvedElementIsSubmitShaped = isClick && !stepDeclaredNonSubmit && resolvedAction?.selector
|
|
12477
|
+
? submitShapeObservedBeforeClick.get(resolvedAction.selector) === true ||
|
|
12478
|
+
submitShapedControlsBeforeClick.has(canonicalAbsoluteXpath(resolvedAction.selector) ?? "") ||
|
|
12479
|
+
(await resolvedClickTargetIsSubmitShaped(frameTarget ?? (0, frame_target_1.mainFrameTarget)(page), resolvedAction.selector))
|
|
12480
|
+
: resolvedAction === null &&
|
|
12481
|
+
record.actResultSuccess === true &&
|
|
12482
|
+
!stepDeclaredNonSubmit &&
|
|
12483
|
+
submitShapeObservedBeforeClick.values().next().value === true;
|
|
12484
|
+
if (resolvedElementIsSubmitShaped)
|
|
12485
|
+
adoptElementStepStartBaseline();
|
|
12486
|
+
const advanceBaseline = stepStartSnapshot ?? pre;
|
|
12487
|
+
// Computed once: gates every credit signal that takes no post-click URL
|
|
12488
|
+
// (network, DOM read-back) and classifyPhantomClick's destinationPlausible.
|
|
12489
|
+
const stepIsMidFlowSubmit = isMidFlowSubmitShapedStep({
|
|
12490
|
+
step,
|
|
12491
|
+
submitStep,
|
|
12492
|
+
isFinalStep,
|
|
12493
|
+
flowHasSubmitSemantics: flowHasSubmitSemanticsFlag,
|
|
12494
|
+
elementSubmitShaped: resolvedElementIsSubmitShaped,
|
|
12495
|
+
});
|
|
12071
12496
|
await page.waitForTimeout(STEP_PAUSE_MS);
|
|
12072
12497
|
// A successful act that reported no actions still ran a click-class action;
|
|
12073
12498
|
// judging it before its round-trip lands would cascade onto the advanced page.
|
|
12074
12499
|
const mayTriggerRoundTrip = resolvedAction === null
|
|
12075
12500
|
? record.actResultSuccess === true
|
|
12076
12501
|
: !STATE_CLASS_METHODS.has(resolvedAction.method ?? "");
|
|
12077
|
-
|
|
12078
|
-
|
|
12079
|
-
|
|
12080
|
-
}
|
|
12081
|
-
|
|
12502
|
+
// Re-settle is not gated on submit shape: an unflagged step or a no-action
|
|
12503
|
+
// act can still advance a page whose body re-renders late. Credit stays
|
|
12504
|
+
// gated on stepIsMidFlowSubmit, so the final step keeps its strictness.
|
|
12505
|
+
const { post } = await readMidFlowPostClickAdvance({
|
|
12506
|
+
target: frameTarget ?? (0, frame_target_1.mainFrameTarget)(page),
|
|
12507
|
+
signalCounter,
|
|
12508
|
+
page,
|
|
12509
|
+
pre,
|
|
12510
|
+
stepStartSnapshot: advanceBaseline,
|
|
12511
|
+
midFlowSubmit: stepIsMidFlowSubmit,
|
|
12512
|
+
mayTriggerRoundTrip,
|
|
12513
|
+
waitForMutations: isSettleWaitStep || resolvedElementIsSubmitShaped,
|
|
12514
|
+
});
|
|
12082
12515
|
record.post = post;
|
|
12083
12516
|
if (resolvedAction) {
|
|
12084
12517
|
record.resolvedMethod = resolvedAction.method ?? null;
|
|
12085
12518
|
record.resolvedArguments = (resolvedAction.arguments ?? []).map(String);
|
|
12086
12519
|
}
|
|
12087
12520
|
const networkFired = post.networkCount > pre.networkCount;
|
|
12088
|
-
// Computed once: gates every credit signal that takes no post-click URL
|
|
12089
|
-
// (network, DOM read-back) and classifyPhantomClick's destinationPlausible.
|
|
12090
|
-
const stepIsMidFlowSubmit = isMidFlowSubmitShapedStep({
|
|
12091
|
-
step,
|
|
12092
|
-
submitStep,
|
|
12093
|
-
isFinalStep,
|
|
12094
|
-
flowHasSubmitSemantics: flowHasSubmitSemanticsFlag,
|
|
12095
|
-
});
|
|
12096
12521
|
const destinationPlausible = (0, phantom_click_1.isPlausibleStepDestination)(step, post.url, stepIsMidFlowSubmit ? pre.url : undefined);
|
|
12097
12522
|
const urlChanged = (0, phantom_click_1.hasOriginOrPathChanged)(pre.url, post.url) && destinationPlausible;
|
|
12098
12523
|
const isStateClass = resolvedAction !== null && STATE_CLASS_METHODS.has(resolvedAction.method ?? "");
|
|
12099
|
-
const isClick = resolvedAction !== null && resolvedAction.method === "click";
|
|
12100
12524
|
// State-class actions (fill/check/etc.) never move the network counter or URL,
|
|
12101
12525
|
// so the legacy heuristic false-negatived every form fill. Re-read DOM state
|
|
12102
12526
|
// for those; keep the navigation signal authoritative for clicks/links —
|
|
@@ -12193,12 +12617,6 @@ async function executeStepWithHealing(params) {
|
|
|
12193
12617
|
const clickedElementStillPresent = isClick && resolvedAction?.selector
|
|
12194
12618
|
? await resolvedClickTargetStillPresent(frameTarget ?? (0, frame_target_1.mainFrameTarget)(page), resolvedAction.selector)
|
|
12195
12619
|
: true;
|
|
12196
|
-
// Attribute-based submit-shape read of the resolved click target itself —
|
|
12197
|
-
// independent of the flow-authored `submitStep` flag. See
|
|
12198
|
-
// resolvedClickTargetIsSubmitShaped's doc comment.
|
|
12199
|
-
const resolvedElementIsSubmitShaped = isClick && !stepDeclaredNonSubmit && resolvedAction?.selector
|
|
12200
|
-
? await resolvedClickTargetIsSubmitShaped(frameTarget ?? (0, frame_target_1.mainFrameTarget)(page), resolvedAction.selector)
|
|
12201
|
-
: false;
|
|
12202
12620
|
// Client-side view-swap gate: credit a click that produces substantial
|
|
12203
12621
|
// DOM growth (≥5KB) with zero network when it's NOT a submit/final step
|
|
12204
12622
|
// and NOT an advance-pattern step. Fixes the top-window site "Manual Application"
|
|
@@ -12215,23 +12633,42 @@ async function executeStepWithHealing(params) {
|
|
|
12215
12633
|
flowHasSubmitSemantics: flowHasSubmitSemanticsFlag,
|
|
12216
12634
|
elementSubmitShaped: resolvedElementIsSubmitShaped,
|
|
12217
12635
|
});
|
|
12636
|
+
// A submit-worded last slot without submit semantics is mid-flow
|
|
12637
|
+
// (isGenuineFinalSubmit), so the sibling predicates below must not hold it
|
|
12638
|
+
// to genuine-final strictness while the verdict gates already credit it on
|
|
12639
|
+
// a settled advance.
|
|
12640
|
+
const primarySubmitShaped = isStepSubmitShaped({
|
|
12641
|
+
step,
|
|
12642
|
+
submitStep,
|
|
12643
|
+
isFinalStep,
|
|
12644
|
+
flowHasSubmitSemantics: flowHasSubmitSemanticsFlag,
|
|
12645
|
+
}) &&
|
|
12646
|
+
(submitStep ||
|
|
12647
|
+
isGenuineFinalSubmit({ isFinalStep, flowHasSubmitSemantics: flowHasSubmitSemanticsFlag }) ||
|
|
12648
|
+
!isFinalStep);
|
|
12218
12649
|
// An element-inferred mid-flow submit joins the exemption the step-derived
|
|
12219
12650
|
// one already got: a click that never left its URL did not bounce to a gate.
|
|
12220
|
-
const
|
|
12221
|
-
|
|
12651
|
+
const midFlowDestinationPlausibleForPost = midFlowDestinationPlausible({
|
|
12652
|
+
step,
|
|
12653
|
+
pre,
|
|
12654
|
+
post,
|
|
12655
|
+
midFlowSubmit,
|
|
12656
|
+
advanceBaseline,
|
|
12657
|
+
destinationPlausible,
|
|
12658
|
+
});
|
|
12222
12659
|
const clickViewSwapVerified = isClickViewSwapVerified({
|
|
12223
12660
|
resolvedAction,
|
|
12224
12661
|
submitStep,
|
|
12225
12662
|
isAdvanceWithPattern: isAdvanceStep(step) && advanceTransitionBodyPattern !== null,
|
|
12226
12663
|
networkDelta: post.networkCount - pre.networkCount,
|
|
12227
12664
|
bytesDelta: post.bodyHtmlLength - pre.bodyHtmlLength,
|
|
12228
|
-
textChanged:
|
|
12665
|
+
textChanged: (0, phantom_click_1.hasVisibleTextChanged)(advanceBaseline, post),
|
|
12229
12666
|
invalidMarkerDelta: postInvalidMarkerCount - preInvalidMarkerCount,
|
|
12230
12667
|
clickedElementStillPresent,
|
|
12231
12668
|
resolvedElementIsSubmitShaped,
|
|
12232
|
-
destinationPlausible:
|
|
12669
|
+
destinationPlausible: midFlowDestinationPlausibleForPost,
|
|
12233
12670
|
midFlowSubmit,
|
|
12234
|
-
pageAdvanced: (0, phantom_click_1.
|
|
12671
|
+
pageAdvanced: (0, phantom_click_1.hasSettledPageAdvanced)(advanceBaseline, post),
|
|
12235
12672
|
actSucceeded: record.actResultSuccess === true,
|
|
12236
12673
|
});
|
|
12237
12674
|
if (clickViewSwapVerified === false &&
|
|
@@ -12265,12 +12702,7 @@ async function executeStepWithHealing(params) {
|
|
|
12265
12702
|
// submitStep, inferred final-step, or an attribute-detected submit
|
|
12266
12703
|
// control) may not ride this signal alone — only when the submit-judge
|
|
12267
12704
|
// gate (requireSubmitEndpoint) will re-litigate the credit below.
|
|
12268
|
-
const formValueWeakSignalAllowed = !(
|
|
12269
|
-
step,
|
|
12270
|
-
submitStep,
|
|
12271
|
-
isFinalStep,
|
|
12272
|
-
flowHasSubmitSemantics: flowHasSubmitSemanticsFlag,
|
|
12273
|
-
}) || resolvedElementIsSubmitShaped) || requireSubmitEndpoint;
|
|
12705
|
+
const formValueWeakSignalAllowed = !(primarySubmitShaped || resolvedElementIsSubmitShaped) || requireSubmitEndpoint;
|
|
12274
12706
|
const formValueVerified = isStateClass &&
|
|
12275
12707
|
formValueWeakSignalAllowed &&
|
|
12276
12708
|
destinationPlausible &&
|
|
@@ -12390,7 +12822,8 @@ async function executeStepWithHealing(params) {
|
|
|
12390
12822
|
flowHasSubmitSemantics: flowHasSubmitSemanticsFlag,
|
|
12391
12823
|
}),
|
|
12392
12824
|
isMidFlowSubmitStep: midFlowSubmit,
|
|
12393
|
-
|
|
12825
|
+
advanceBaseline,
|
|
12826
|
+
destinationPlausible: midFlowDestinationPlausibleForPost,
|
|
12394
12827
|
});
|
|
12395
12828
|
// An `"effective"` verdict driven purely by the page-wide byte-delta
|
|
12396
12829
|
// floor (`TRIVIAL_DOM_DELTA_BYTES`, 500B) is intentionally NOT trusted
|
|
@@ -12411,12 +12844,7 @@ async function executeStepWithHealing(params) {
|
|
|
12411
12844
|
// respects the SAME DOM-only-advance veto as the block above: a wizard
|
|
12412
12845
|
// "Next" step on a pattern-configured site whose only signal is a field
|
|
12413
12846
|
// toggle must stay unverified, not get waved through by the verdict.
|
|
12414
|
-
const domEffectiveVerdict = !
|
|
12415
|
-
step,
|
|
12416
|
-
submitStep,
|
|
12417
|
-
isFinalStep,
|
|
12418
|
-
flowHasSubmitSemantics: flowHasSubmitSemanticsFlag,
|
|
12419
|
-
}) && domVerifiedForStep;
|
|
12847
|
+
const domEffectiveVerdict = !primarySubmitShaped && domVerifiedForStep;
|
|
12420
12848
|
let verified = networkIsRealAdvance ||
|
|
12421
12849
|
urlChanged ||
|
|
12422
12850
|
domVerifiedForStep ||
|
|
@@ -12499,11 +12927,17 @@ async function executeStepWithHealing(params) {
|
|
|
12499
12927
|
captureFn,
|
|
12500
12928
|
step,
|
|
12501
12929
|
isFinalStep,
|
|
12930
|
+
submitStep,
|
|
12931
|
+
flowHasSubmitSemantics: flowHasSubmitSemanticsFlag,
|
|
12932
|
+
elementSubmitShaped: resolvedElementIsSubmitShaped,
|
|
12502
12933
|
page,
|
|
12503
12934
|
target: frameTarget ?? (0, frame_target_1.mainFrameTarget)(page),
|
|
12504
12935
|
signalCounter,
|
|
12505
12936
|
preNetworkCount: pre.networkCount,
|
|
12506
|
-
preVisibleTextSignature:
|
|
12937
|
+
preVisibleTextSignature: advanceBaseline.visibleTextSignature,
|
|
12938
|
+
preVisibleText: advanceBaseline.visibleText,
|
|
12939
|
+
preVisibleTextDigest: advanceBaseline.visibleTextDigest,
|
|
12940
|
+
settledPostVisibleText: post.visibleText,
|
|
12507
12941
|
submitInput: {
|
|
12508
12942
|
pageUrl: post.url,
|
|
12509
12943
|
pageTitle,
|
|
@@ -12522,14 +12956,14 @@ async function executeStepWithHealing(params) {
|
|
|
12522
12956
|
// A mid-flow transition is credited on the page's own demonstrable
|
|
12523
12957
|
// advance (visible text swapped); the judge's final-submit-flavoured
|
|
12524
12958
|
// view of a stale title or missing network must not undo it.
|
|
12525
|
-
const midFlowAdvanceCredited = (
|
|
12526
|
-
|
|
12527
|
-
|
|
12528
|
-
|
|
12529
|
-
|
|
12530
|
-
|
|
12531
|
-
|
|
12532
|
-
|
|
12959
|
+
const midFlowAdvanceCredited = isMidFlowAdvanceCredited({
|
|
12960
|
+
midFlowSubmit,
|
|
12961
|
+
signalAdvanced: clickViewSwapVerified,
|
|
12962
|
+
judgeUnavailable: judgeVerdict === null,
|
|
12963
|
+
networkAdvanced: networkIsRealAdvance,
|
|
12964
|
+
pageAdvanced: (0, phantom_click_1.hasSettledPageAdvanced)(advanceBaseline, post),
|
|
12965
|
+
invalidMarkerCount,
|
|
12966
|
+
});
|
|
12533
12967
|
if (midFlowAdvanceCredited && judgeVerdict?.verified !== true) {
|
|
12534
12968
|
logger.info(`mid-flow submit credited on demonstrable page advance (judge ${judgeVerdict === null ? "unavailable" : `rejected: ${judgeVerdict.reason}`})`);
|
|
12535
12969
|
}
|
|
@@ -12565,7 +12999,7 @@ async function executeStepWithHealing(params) {
|
|
|
12565
12999
|
// Any-4xx fallback. The judge said failed; surface any captured 4xx
|
|
12566
13000
|
// body's field-level error JSON for the rephrase prompt downstream
|
|
12567
13001
|
// — same role as before the migration.
|
|
12568
|
-
const fallbackEvidence = extractSubmitFailureEvidence(recentCaptures.slice(-tail.length), [], (0,
|
|
13002
|
+
const fallbackEvidence = extractSubmitFailureEvidence(recentCaptures.slice(-tail.length), [], (0, runtime_capture_store_1.defaultCapturesDir)(), "any-4xx");
|
|
12569
13003
|
if (fallbackEvidence.length > 0) {
|
|
12570
13004
|
failureReasons.push(`any-4xx fallback: ${fallbackEvidence.split("\n")[0]}`.slice(0, 240));
|
|
12571
13005
|
}
|
|
@@ -12740,20 +13174,22 @@ async function executeStepWithHealing(params) {
|
|
|
12740
13174
|
probeResult.checked === true &&
|
|
12741
13175
|
(probeResult.ariaManaged === true || !ancestorStillInvalid);
|
|
12742
13176
|
await page.waitForTimeout(STEP_PAUSE_MS);
|
|
12743
|
-
|
|
12744
|
-
const retryPost
|
|
12745
|
-
|
|
12746
|
-
|
|
12747
|
-
|
|
12748
|
-
|
|
12749
|
-
|
|
12750
|
-
|
|
12751
|
-
|
|
13177
|
+
const { midFlowSubmit: retryMidFlowSubmit, advanceBaseline: retryAdvanceBaseline } = deriveMidFlowJudgement(retryResolvedElementIsSubmitShaped, pre);
|
|
13178
|
+
const { post: retryPost, pageAdvanced: retryPageAdvanced } = await readMidFlowPostClickAdvance({
|
|
13179
|
+
target: frameTarget ?? (0, frame_target_1.mainFrameTarget)(page),
|
|
13180
|
+
signalCounter,
|
|
13181
|
+
page,
|
|
13182
|
+
pre,
|
|
13183
|
+
stepStartSnapshot: retryAdvanceBaseline,
|
|
13184
|
+
midFlowSubmit: retryMidFlowSubmit,
|
|
13185
|
+
waitForMutations: isSettleWaitStep || retryResolvedElementIsSubmitShaped,
|
|
12752
13186
|
});
|
|
13187
|
+
const retryNetworkFired = retryPost.networkCount > pre.networkCount;
|
|
12753
13188
|
const retryUrlChanged = (0, phantom_click_1.hasOriginOrPathChanged)(pre.url, retryPost.url) &&
|
|
12754
13189
|
(0, phantom_click_1.isPlausibleStepDestination)(step, retryPost.url, retryMidFlowSubmit ? pre.url : undefined);
|
|
12755
|
-
const
|
|
12756
|
-
const
|
|
13190
|
+
const retryDomBaseline = retryMidFlowSubmit ? retryAdvanceBaseline : pre;
|
|
13191
|
+
const retryHtmlDelta = retryPost.bodyHtmlLength - retryDomBaseline.bodyHtmlLength;
|
|
13192
|
+
const retryTextChanged = (0, phantom_click_1.hasVisibleTextChanged)(retryDomBaseline, retryPost);
|
|
12757
13193
|
const retryFormValueChanged = retryPost.formValueSignature !== pre.formValueSignature;
|
|
12758
13194
|
// Element-scoped selection read-back for the n+16 fallback — the same
|
|
12759
13195
|
// authoritative signal the primary verifier uses via `verifyDomEffect`,
|
|
@@ -12823,6 +13259,8 @@ async function executeStepWithHealing(params) {
|
|
|
12823
13259
|
.evaluate(disabledExpr)
|
|
12824
13260
|
.catch(() => false);
|
|
12825
13261
|
})());
|
|
13262
|
+
if (probeResult.kind === "click")
|
|
13263
|
+
lastClickBlockedByDisabled = clickBlockedByDisabled;
|
|
12826
13264
|
const clickWasDomOnly = probeResult.kind === "click" && !retryNetworkFired && !retryUrlChanged;
|
|
12827
13265
|
const clickBlockedByInvalid = clickWasDomOnly &&
|
|
12828
13266
|
(await countNgInvalidContainers(frameTarget ?? (0, frame_target_1.mainFrameTarget)(page)).catch(() => 0)) >
|
|
@@ -12848,10 +13286,14 @@ async function executeStepWithHealing(params) {
|
|
|
12848
13286
|
const retryDestinationPlausible = (0, phantom_click_1.isPlausibleStepDestination)(step, retryPost.url);
|
|
12849
13287
|
// Only the verdict sees the pre.url exemption, and only for a demonstrable
|
|
12850
13288
|
// page advance: bare network/DOM signals stay gated on the landed URL.
|
|
12851
|
-
const retryMidFlowDestinationPlausible =
|
|
12852
|
-
|
|
12853
|
-
|
|
12854
|
-
|
|
13289
|
+
const retryMidFlowDestinationPlausible = midFlowDestinationPlausible({
|
|
13290
|
+
step,
|
|
13291
|
+
pre,
|
|
13292
|
+
post: retryPost,
|
|
13293
|
+
midFlowSubmit: retryMidFlowSubmit,
|
|
13294
|
+
advanceBaseline: retryAdvanceBaseline,
|
|
13295
|
+
destinationPlausible: retryDestinationPlausible,
|
|
13296
|
+
});
|
|
12855
13297
|
const retryNetworkIsRealAdvance = retryNetworkFired &&
|
|
12856
13298
|
retryDestinationPlausible &&
|
|
12857
13299
|
(await waitForTransitionBody({
|
|
@@ -12959,6 +13401,7 @@ async function executeStepWithHealing(params) {
|
|
|
12959
13401
|
elementStateChanged: retrySelectionStateChanged,
|
|
12960
13402
|
isSubmitShapedStep: retryMidFlowSubmit || retrySubmitShaped,
|
|
12961
13403
|
isMidFlowSubmitStep: retryMidFlowSubmit,
|
|
13404
|
+
advanceBaseline: retryAdvanceBaseline,
|
|
12962
13405
|
destinationPlausible: retryMidFlowDestinationPlausible,
|
|
12963
13406
|
});
|
|
12964
13407
|
// Destination-plausibility audit of every remaining disjunct in
|
|
@@ -12995,6 +13438,10 @@ async function executeStepWithHealing(params) {
|
|
|
12995
13438
|
(n16ClickDelivered &&
|
|
12996
13439
|
(!retrySubmitShaped || retryMidFlowSubmit) &&
|
|
12997
13440
|
retryVerdict === "effective") ||
|
|
13441
|
+
(n16ClickDelivered &&
|
|
13442
|
+
retryMidFlowSubmit &&
|
|
13443
|
+
retryPageAdvanced &&
|
|
13444
|
+
retryMidFlowDestinationPlausible) ||
|
|
12998
13445
|
(n16ClickDelivered &&
|
|
12999
13446
|
weakDomSignalsAllowed &&
|
|
13000
13447
|
retryDestinationPlausible &&
|
|
@@ -13092,11 +13539,17 @@ async function executeStepWithHealing(params) {
|
|
|
13092
13539
|
captureFn,
|
|
13093
13540
|
step,
|
|
13094
13541
|
isFinalStep,
|
|
13542
|
+
submitStep,
|
|
13543
|
+
flowHasSubmitSemantics: flowHasSubmitSemanticsFlag,
|
|
13544
|
+
elementSubmitShaped: retryResolvedElementIsSubmitShaped,
|
|
13095
13545
|
page,
|
|
13096
13546
|
target: frameTarget ?? (0, frame_target_1.mainFrameTarget)(page),
|
|
13097
13547
|
signalCounter,
|
|
13098
13548
|
preNetworkCount: pre.networkCount,
|
|
13099
|
-
preVisibleTextSignature:
|
|
13549
|
+
preVisibleTextSignature: retryAdvanceBaseline.visibleTextSignature,
|
|
13550
|
+
preVisibleText: retryAdvanceBaseline.visibleText,
|
|
13551
|
+
preVisibleTextDigest: retryAdvanceBaseline.visibleTextDigest,
|
|
13552
|
+
settledPostVisibleText: retryPost.visibleText,
|
|
13100
13553
|
submitInput: {
|
|
13101
13554
|
pageUrl: retryPost.url,
|
|
13102
13555
|
pageTitle,
|
|
@@ -13112,16 +13565,14 @@ async function executeStepWithHealing(params) {
|
|
|
13112
13565
|
submittedStateSelectors: matchedSubmittedSelectors,
|
|
13113
13566
|
},
|
|
13114
13567
|
});
|
|
13115
|
-
const retryMidFlowAdvanceCredited = (
|
|
13116
|
-
|
|
13117
|
-
|
|
13118
|
-
|
|
13119
|
-
|
|
13120
|
-
|
|
13121
|
-
|
|
13122
|
-
|
|
13123
|
-
invalidMarkerCount,
|
|
13124
|
-
});
|
|
13568
|
+
const retryMidFlowAdvanceCredited = isMidFlowAdvanceCredited({
|
|
13569
|
+
midFlowSubmit: retryMidFlowSubmit,
|
|
13570
|
+
signalAdvanced: retryVerdict === "effective" && retryPageAdvanced,
|
|
13571
|
+
judgeUnavailable: judgeVerdict === null,
|
|
13572
|
+
networkAdvanced: retryNetworkFired && retryDestinationPlausible,
|
|
13573
|
+
pageAdvanced: retryPageAdvanced,
|
|
13574
|
+
invalidMarkerCount,
|
|
13575
|
+
});
|
|
13125
13576
|
if (retryMidFlowAdvanceCredited && judgeVerdict?.verified !== true) {
|
|
13126
13577
|
logger.info(`n+16 fallback mid-flow submit credited on demonstrable page advance (judge ${judgeVerdict === null ? "unavailable" : `rejected: ${judgeVerdict.reason}`})`);
|
|
13127
13578
|
}
|
|
@@ -13300,12 +13751,7 @@ async function executeStepWithHealing(params) {
|
|
|
13300
13751
|
// (triedSelectors), which is empty on an "unresolved" verdict, so a
|
|
13301
13752
|
// non-submit step with that verdict falls through to the normal
|
|
13302
13753
|
// ladder instead of escalating.
|
|
13303
|
-
const escalationTarget =
|
|
13304
|
-
step,
|
|
13305
|
-
submitStep,
|
|
13306
|
-
isFinalStep,
|
|
13307
|
-
flowHasSubmitSemantics: flowHasSubmitSemanticsFlag,
|
|
13308
|
-
})
|
|
13754
|
+
const escalationTarget = primarySubmitShaped
|
|
13309
13755
|
? "escalating attempt 2 to deep-submit-locator"
|
|
13310
13756
|
: phantomClickAfterAttempt1
|
|
13311
13757
|
? "non-submit step — escalating attempt 2 to trusted-click-retry (trusted CDP click on the resolved target)"
|
|
@@ -13317,17 +13763,16 @@ async function executeStepWithHealing(params) {
|
|
|
13317
13763
|
// Treat the canonical submit click as "final" for this predicate
|
|
13318
13764
|
// even when it lives mid-flow. See requireSubmitEndpoint derivation
|
|
13319
13765
|
// above for the same gate-widening rationale.
|
|
13320
|
-
isFinalStep:
|
|
13321
|
-
step,
|
|
13322
|
-
submitStep,
|
|
13323
|
-
isFinalStep,
|
|
13324
|
-
flowHasSubmitSemantics: flowHasSubmitSemanticsFlag,
|
|
13325
|
-
}),
|
|
13766
|
+
isFinalStep: primarySubmitShaped,
|
|
13326
13767
|
requireSubmitEndpoint,
|
|
13327
13768
|
resolvedMethod: record.resolvedMethod,
|
|
13328
13769
|
effectSignals,
|
|
13329
13770
|
preSubmitInvalidCount,
|
|
13330
13771
|
postAttemptInvalidCount,
|
|
13772
|
+
midFlowAdvanced: !isGenuineFinalSubmit({
|
|
13773
|
+
isFinalStep,
|
|
13774
|
+
flowHasSubmitSemantics: flowHasSubmitSemanticsFlag,
|
|
13775
|
+
}) && (0, phantom_click_1.hasSettledPageAdvanced)(advanceBaseline, post),
|
|
13331
13776
|
});
|
|
13332
13777
|
if (earlyExit) {
|
|
13333
13778
|
const exitReason = `submit-revealed-invalid: click surfaced ${postAttemptInvalidCount - preSubmitInvalidCount} new ng-invalid container(s) (was ${preSubmitInvalidCount}, now ${postAttemptInvalidCount}); attempts 2-${MAX_STEP_ATTEMPTS} cannot heal a form that needs answers — routing to replan`;
|
|
@@ -13389,6 +13834,7 @@ async function executeStepWithHealing(params) {
|
|
|
13389
13834
|
submitStep,
|
|
13390
13835
|
isFinalStep,
|
|
13391
13836
|
flowHasSubmitSemantics: flowHasSubmitSemanticsFlag,
|
|
13837
|
+
elementSubmitShaped: !stepDeclaredNonSubmit,
|
|
13392
13838
|
}),
|
|
13393
13839
|
});
|
|
13394
13840
|
if (fallbackTarget) {
|
|
@@ -13397,6 +13843,8 @@ async function executeStepWithHealing(params) {
|
|
|
13397
13843
|
return "completed";
|
|
13398
13844
|
}
|
|
13399
13845
|
}
|
|
13846
|
+
if (await creditStepStartAdvance("cascade-exhausted"))
|
|
13847
|
+
return "completed";
|
|
13400
13848
|
const cascadeExhaustObservedFinal = await (0, stagehand_guard_1.guardedObserve)(stagehand, step, { timeout: exports.STEP_WATCHDOG_MS }, captureFn, frameTarget).catch(() => []);
|
|
13401
13849
|
// observe() is blind to a cross-origin OOPIF, so a frame-scoped empty
|
|
13402
13850
|
// result degrades to the deep-locator resolver for dump evidence — this
|
|
@@ -13492,6 +13940,9 @@ async function executeNavigateStep(params) {
|
|
|
13492
13940
|
* than let the flow report phantom success.
|
|
13493
13941
|
*/
|
|
13494
13942
|
async function runHealingFlow(deps) {
|
|
13943
|
+
return (0, runtime_capture_store_1.withRuntimeCaptureStore)((store) => runHealingFlowScoped(deps, store));
|
|
13944
|
+
}
|
|
13945
|
+
async function runHealingFlowScoped(deps, store) {
|
|
13495
13946
|
const { stagehand, page, steps, logger, anthropic, rephraseModel, uploadFixture, maxFlowMs } = deps;
|
|
13496
13947
|
for (const secret of deps.secrets ?? [])
|
|
13497
13948
|
(0, pii_redaction_1.registerSecretValue)(secret);
|
|
@@ -13521,6 +13972,7 @@ async function runHealingFlow(deps) {
|
|
|
13521
13972
|
recentCaptures,
|
|
13522
13973
|
recentCaptureMeta,
|
|
13523
13974
|
getCurrentPhase: () => "flow",
|
|
13975
|
+
onCapture: (capture, filename) => (0, runtime_capture_store_1.recordRuntimeCapture)(store, filename, capture),
|
|
13524
13976
|
getCurrentPageOrigin: () => {
|
|
13525
13977
|
try {
|
|
13526
13978
|
return new URL(activePage.url()).origin;
|