@enricai/barnacle 1.12.80 → 1.12.82
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/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 +2 -1
- package/dist/lib/llm/judges/verify-step-advanced.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 +41 -1
- package/dist/scraper/click-settle.d.ts.map +1 -1
- package/dist/scraper/click-settle.js +84 -15
- package/dist/scraper/click-settle.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 +68 -33
- package/dist/scraper/flow-runner.d.ts.map +1 -1
- package/dist/scraper/flow-runner.js +610 -198
- 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 +6 -0
- package/dist/scraper/phantom-click.d.ts.map +1 -1
- package/dist/scraper/phantom-click.js +62 -15
- package/dist/scraper/phantom-click.js.map +1 -1
- package/dist/scraper/stagehand-guard.d.ts +12 -2
- package/dist/scraper/stagehand-guard.d.ts.map +1 -1
- package/dist/scraper/stagehand-guard.js +51 -33
- package/dist/scraper/stagehand-guard.js.map +1 -1
- package/dist/scraper/trusted-click.d.ts +12 -1
- package/dist/scraper/trusted-click.d.ts.map +1 -1
- package/dist/scraper/trusted-click.js +8 -4
- package/dist/scraper/trusted-click.js.map +1 -1
- package/dist/scraper/upload-affordance-expr.js +1 -1
- package/dist/scraper/upload-affordance-expr.js.map +1 -1
- package/dist/scripts/recon-generate.d.ts +5 -7
- package/dist/scripts/recon-generate.d.ts.map +1 -1
- package/dist/scripts/recon-generate.js +123 -52
- package/dist/scripts/recon-generate.js.map +1 -1
- package/dist/scripts/recon-shared.d.ts +3 -2
- package/dist/scripts/recon-shared.d.ts.map +1 -1
- package/dist/scripts/recon-shared.js +6 -11
- package/dist/scripts/recon-shared.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
|
@@ -21,6 +21,7 @@ exports.shouldRetryCaptchaRegistry = shouldRetryCaptchaRegistry;
|
|
|
21
21
|
exports.buildDeepMarkerCentreExpr = buildDeepMarkerCentreExpr;
|
|
22
22
|
exports.prepareFailureDumpBody = prepareFailureDumpBody;
|
|
23
23
|
exports.fieldValuesAtFailureExpr = fieldValuesAtFailureExpr;
|
|
24
|
+
exports.settleWithPageQuiescence = settleWithPageQuiescence;
|
|
24
25
|
exports.snapshotPage = snapshotPage;
|
|
25
26
|
exports.resolveDumpPageIdentity = resolveDumpPageIdentity;
|
|
26
27
|
exports.findRecentBackendError = findRecentBackendError;
|
|
@@ -77,6 +78,7 @@ exports.verifyFillReadback = verifyFillReadback;
|
|
|
77
78
|
exports.verifyPromptSelectorCommitted = verifyPromptSelectorCommitted;
|
|
78
79
|
exports.tailRetargetPhrasesForStep = tailRetargetPhrasesForStep;
|
|
79
80
|
exports.stepQuotesValueNotControlName = stepQuotesValueNotControlName;
|
|
81
|
+
exports.waitForUploadNetworkSignal = waitForUploadNetworkSignal;
|
|
80
82
|
exports.isUploadAffordanceLabel = isUploadAffordanceLabel;
|
|
81
83
|
exports.writeFixtureToTempFile = writeFixtureToTempFile;
|
|
82
84
|
exports.attachToSurfacedInput = attachToSurfacedInput;
|
|
@@ -88,6 +90,7 @@ exports.parseFillStep = parseFillStep;
|
|
|
88
90
|
exports.parseFillValueIntent = parseFillValueIntent;
|
|
89
91
|
exports.parseSegmentedDateValue = parseSegmentedDateValue;
|
|
90
92
|
exports.resolveSegmentedDateGroup = resolveSegmentedDateGroup;
|
|
93
|
+
exports.parseSegmentedPartStep = parseSegmentedPartStep;
|
|
91
94
|
exports.trySegmentedDateSpinbuttonPrimitive = trySegmentedDateSpinbuttonPrimitive;
|
|
92
95
|
exports.parseRadioStep = parseRadioStep;
|
|
93
96
|
exports.parseWidgetOptionClickStep = parseWidgetOptionClickStep;
|
|
@@ -391,6 +394,8 @@ function wireSignalCapture(page, params) {
|
|
|
391
394
|
method: capture.method,
|
|
392
395
|
status: capture.status,
|
|
393
396
|
url: capture.url,
|
|
397
|
+
hasBody: (req.requestPostData ?? "").length > 0,
|
|
398
|
+
multipart: /^multipart\//i.test(requestContentType(req.requestHeaders)),
|
|
394
399
|
});
|
|
395
400
|
if (recentCaptureMeta.length > RECENT_CAPTURES_WINDOW) {
|
|
396
401
|
recentCaptureMeta.splice(0, recentCaptureMeta.length - RECENT_CAPTURES_WINDOW);
|
|
@@ -733,6 +738,18 @@ const PROMPT_CURRENT_TEXT_EXPR = `((w, valueSel, emptyRx) => {
|
|
|
733
738
|
if (raw.trim() && !emptyRx.test(raw.trim())) return norm(raw);
|
|
734
739
|
return "";
|
|
735
740
|
})`;
|
|
741
|
+
/**
|
|
742
|
+
* Browser-side predicate: does a widget's committed text name the requested
|
|
743
|
+
* option as a whole phrase? A bare substring test credits "Astrophysics" for
|
|
744
|
+
* "Physics"; comparing on word boundaries (punctuation folded to spaces) keeps
|
|
745
|
+
* chip lists and "N items selected, X" announcements matching while a longer,
|
|
746
|
+
* different option no longer does.
|
|
747
|
+
*/
|
|
748
|
+
const PROMPT_NAMES_OPTION_EXPR = `((committed, wanted) => {
|
|
749
|
+
const words = (s) => " " + (s || "").toLowerCase().replace(/[^a-z0-9]+/g, " ").trim() + " ";
|
|
750
|
+
const want = words(wanted);
|
|
751
|
+
return want.trim() === "" || words(committed).includes(want);
|
|
752
|
+
})`;
|
|
736
753
|
/**
|
|
737
754
|
* Browser-side expression reading what a prompt-selector widget ANNOUNCES it
|
|
738
755
|
* committed, lowercased and whitespace-collapsed, or `""` when nothing is
|
|
@@ -764,6 +781,8 @@ const PROMPT_ANNOUNCED_VALUE_EXPR = `((w) => {
|
|
|
764
781
|
let anode = w;
|
|
765
782
|
for (let d = 0; d < ${browser_click_expr_1.MAX_SELECTION_ANCESTOR_DEPTH} && anode; d++) {
|
|
766
783
|
const list = anode.querySelector ? anode.querySelector('[data-automation-id="selectedItemList"]') : null;
|
|
784
|
+
const entries = list ? Array.from(list.querySelectorAll("*")).filter((e) => e.children.length === 0).map((e) => norm(e.textContent)).filter(Boolean) : [];
|
|
785
|
+
if (entries.length > 0) return entries.join(" , ");
|
|
767
786
|
if (list && norm(list.textContent)) return norm(list.textContent);
|
|
768
787
|
anode = anode.parentElement;
|
|
769
788
|
}
|
|
@@ -933,26 +952,15 @@ const N16_TRUSTED_CLICK_TIMEOUT_MS = 3_000;
|
|
|
933
952
|
/**
|
|
934
953
|
* Shared pre-click centre + hit-test for a Stagehand-resolved selector, so
|
|
935
954
|
* observe-act and the natural-language act attempts share one routine.
|
|
936
|
-
* Returns a failure message when the control
|
|
955
|
+
* Returns a failure message when the control cannot be centred and hit-tested
|
|
956
|
+
* (covered, unresolved, or the page cannot answer the probe), null otherwise.
|
|
937
957
|
*/
|
|
938
|
-
async function centreObservedClickTarget(target, selector
|
|
958
|
+
async function centreObservedClickTarget(target, selector) {
|
|
939
959
|
try {
|
|
940
960
|
await (0, trusted_click_1.centreSelectorForClick)(target, selector);
|
|
941
961
|
return null;
|
|
942
962
|
}
|
|
943
963
|
catch (err) {
|
|
944
|
-
// A TypeError means the page cannot answer the hit-test probe (no
|
|
945
|
-
// evaluate seam), not that the control is covered; defer to the act.
|
|
946
|
-
if (err instanceof TypeError) {
|
|
947
|
-
onUnavailable(`centre hit-test unavailable: ${(0, errors_1.toErrorMessage)(err)}`);
|
|
948
|
-
return null;
|
|
949
|
-
}
|
|
950
|
-
// Stagehand's own resolution of a natural-language act can differ from the
|
|
951
|
-
// observe result, so an unresolvable element there is the act's to judge.
|
|
952
|
-
if (deferWhenMissing && (0, errors_1.toErrorMessage)(err).includes("is not in the document")) {
|
|
953
|
-
onUnavailable(`centre target unresolved: ${(0, errors_1.toErrorMessage)(err)}`);
|
|
954
|
-
return null;
|
|
955
|
-
}
|
|
956
964
|
return `observed click target not clickable: ${(0, errors_1.toErrorMessage)(err)}`;
|
|
957
965
|
}
|
|
958
966
|
}
|
|
@@ -1151,6 +1159,61 @@ function fieldValuesAtFailureExpr() {
|
|
|
1151
1159
|
return out;
|
|
1152
1160
|
})()`;
|
|
1153
1161
|
}
|
|
1162
|
+
/**
|
|
1163
|
+
* Reads the in-page quiescence probe as a typed, non-null value. A detached
|
|
1164
|
+
* frame or malformed probe result reads as idle so settle consumes the reader
|
|
1165
|
+
* directly and needs no guard of its own.
|
|
1166
|
+
*/
|
|
1167
|
+
async function readPageQuiescence(target) {
|
|
1168
|
+
const idle = { pending: 0, mutations: 0 };
|
|
1169
|
+
const result = await Promise.resolve(target.evaluate(click_settle_1.QUIESCENCE_PROBE_EXPR)).catch(() => idle);
|
|
1170
|
+
if (result === null || typeof result !== "object")
|
|
1171
|
+
return idle;
|
|
1172
|
+
const { pending, mutations } = result;
|
|
1173
|
+
return typeof pending === "number" && typeof mutations === "number"
|
|
1174
|
+
? { pending, mutations }
|
|
1175
|
+
: idle;
|
|
1176
|
+
}
|
|
1177
|
+
/**
|
|
1178
|
+
* Reads only the visible-text signature settle polls on. Going through
|
|
1179
|
+
* {@link snapshotPage} also read the frame URL, which rejects once a submit
|
|
1180
|
+
* detaches the OOPIF and would abort the settle mid-poll; a failed read here
|
|
1181
|
+
* degrades to the empty signature like the snapshot's own text read does.
|
|
1182
|
+
*/
|
|
1183
|
+
async function readSettleTextSignature(target) {
|
|
1184
|
+
const result = await Promise.resolve(target.evaluate(DOM_SNAPSHOT_EXPR)).catch(() => null);
|
|
1185
|
+
if (result === null || typeof result !== "object")
|
|
1186
|
+
return "";
|
|
1187
|
+
const { text } = result;
|
|
1188
|
+
return typeof text === "string" ? text : "";
|
|
1189
|
+
}
|
|
1190
|
+
/**
|
|
1191
|
+
* Settles after a click using the target's own text signature and in-page
|
|
1192
|
+
* quiescence reader, so every post-click site decides from the same settle
|
|
1193
|
+
* signal instead of hand-building the readers.
|
|
1194
|
+
*/
|
|
1195
|
+
async function settleWithPageQuiescence(page, target, signalCounter, preNetworkCount, submitShaped, preVisibleTextSignature, advanced) {
|
|
1196
|
+
await (0, click_settle_1.settleAfterClick)(page, signalCounter, preNetworkCount, submitShaped, () => readSettleTextSignature(target), preVisibleTextSignature, () => readPageQuiescence(target), advanced);
|
|
1197
|
+
}
|
|
1198
|
+
/**
|
|
1199
|
+
* Builds the "has the page finished advancing" test a mid-flow submit settles
|
|
1200
|
+
* against. It is the body-swap half of the {@link hasPageAdvanced} the phantom
|
|
1201
|
+
* classifier credits with, so a header that flipped before its body re-rendered
|
|
1202
|
+
* keeps settle polling while the classifier still credits whatever is left once
|
|
1203
|
+
* the wait is spent.
|
|
1204
|
+
* Undefined for any other step, which keeps the plain quiet-window settle.
|
|
1205
|
+
*/
|
|
1206
|
+
function pageAdvancedSince(preVisibleTextSignature, midFlowSubmit) {
|
|
1207
|
+
if (!midFlowSubmit)
|
|
1208
|
+
return undefined;
|
|
1209
|
+
const pre = {
|
|
1210
|
+
networkCount: 0,
|
|
1211
|
+
url: "",
|
|
1212
|
+
bodyHtmlLength: 0,
|
|
1213
|
+
visibleTextSignature: preVisibleTextSignature,
|
|
1214
|
+
};
|
|
1215
|
+
return (visibleTextSignature) => (0, phantom_click_1.hasPageBodySwapped)(pre, { ...pre, visibleTextSignature });
|
|
1216
|
+
}
|
|
1154
1217
|
/**
|
|
1155
1218
|
* Captures the pre/post signal triple the submit-verify cascade diffs.
|
|
1156
1219
|
* Accepts the optional `page` so a resolved child `FrameTarget` whose
|
|
@@ -2902,10 +2965,18 @@ async function judgeStepOutcome(params) {
|
|
|
2902
2965
|
captureFn,
|
|
2903
2966
|
});
|
|
2904
2967
|
}
|
|
2905
|
-
await (
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2968
|
+
await settleWithPageQuiescence(params.page, target, params.signalCounter, params.preNetworkCount, isStepSettleWaitShaped({
|
|
2969
|
+
step,
|
|
2970
|
+
submitStep: params.submitStep,
|
|
2971
|
+
isFinalStep,
|
|
2972
|
+
flowHasSubmitSemantics: params.flowHasSubmitSemantics,
|
|
2973
|
+
}) || params.elementSubmitShaped === true, params.preVisibleTextSignature, pageAdvancedSince(params.preVisibleTextSignature, isMidFlowSubmitShapedStep({
|
|
2974
|
+
step,
|
|
2975
|
+
submitStep: params.submitStep,
|
|
2976
|
+
isFinalStep,
|
|
2977
|
+
flowHasSubmitSemantics: params.flowHasSubmitSemantics,
|
|
2978
|
+
elementSubmitShaped: params.elementSubmitShaped,
|
|
2979
|
+
})));
|
|
2909
2980
|
const colon = params.preVisibleTextSignature.indexOf(":");
|
|
2910
2981
|
const beforeText = (colon === -1 ? params.preVisibleTextSignature : params.preVisibleTextSignature.slice(colon + 1))
|
|
2911
2982
|
.replace(/\s+/g, " ")
|
|
@@ -3557,8 +3628,8 @@ async function verifyFillReadback(target, selector, expectedValue) {
|
|
|
3557
3628
|
if (!el) return null;
|
|
3558
3629
|
const tag = el.tagName ? el.tagName.toLowerCase() : "";
|
|
3559
3630
|
let actual = "";
|
|
3560
|
-
|
|
3561
|
-
if (el
|
|
3631
|
+
${SEGMENT_OWNER_JS}
|
|
3632
|
+
if (isDateSegment(el)) {
|
|
3562
3633
|
return { outcome: "segment", postValue: el.value || el.getAttribute("aria-valuenow") || "", tag };
|
|
3563
3634
|
}
|
|
3564
3635
|
if (el.tagName === "INPUT" || el.tagName === "TEXTAREA") {
|
|
@@ -3624,8 +3695,8 @@ async function verifyPromptSelectorCommitted(target, selector, requestedOption)
|
|
|
3624
3695
|
const emptyRx = new RegExp(${JSON.stringify(PROMPT_EMPTY_VALUE_RX_SRC)}, ${JSON.stringify(PROMPT_EMPTY_VALUE_RX_FLAGS)});
|
|
3625
3696
|
const currentText = ${PROMPT_CURRENT_TEXT_EXPR};
|
|
3626
3697
|
const announcedValue = ${PROMPT_ANNOUNCED_VALUE_EXPR};
|
|
3627
|
-
const
|
|
3628
|
-
const want =
|
|
3698
|
+
const namesOption = ${PROMPT_NAMES_OPTION_EXPR};
|
|
3699
|
+
const want = ${JSON.stringify(requestedOption ?? "")};
|
|
3629
3700
|
const xpath = ${JSON.stringify(xpath)};
|
|
3630
3701
|
const result = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
|
|
3631
3702
|
const el = result.singleNodeValue;
|
|
@@ -3633,7 +3704,7 @@ async function verifyPromptSelectorCommitted(target, selector, requestedOption)
|
|
|
3633
3704
|
const widget = el.matches && el.matches(triggerSel) ? el : el.closest(triggerSel);
|
|
3634
3705
|
if (!widget) return { isPromptWidget: false, committed: false };
|
|
3635
3706
|
const value = announcedValue(widget) || currentText(widget, valueSel, emptyRx);
|
|
3636
|
-
return { isPromptWidget: true, committed: value !== "" && (!want ||
|
|
3707
|
+
return { isPromptWidget: true, committed: value !== "" && (!want || namesOption(value, want)) };
|
|
3637
3708
|
})()`;
|
|
3638
3709
|
try {
|
|
3639
3710
|
const raw = await target.evaluate(expr);
|
|
@@ -4510,12 +4581,17 @@ const UPLOAD_URL_PATTERNS = [
|
|
|
4510
4581
|
"/media",
|
|
4511
4582
|
"/import",
|
|
4512
4583
|
];
|
|
4584
|
+
/** Case-insensitive Content-Type lookup; CDP preserves the page's header casing. */
|
|
4585
|
+
function requestContentType(headers) {
|
|
4586
|
+
const key = Object.keys(headers).find((k) => k.toLowerCase() === "content-type");
|
|
4587
|
+
return key ? (headers[key] ?? "") : "";
|
|
4588
|
+
}
|
|
4513
4589
|
/**
|
|
4514
4590
|
* Poll the recent-capture window for an upload-related POST after a file has
|
|
4515
4591
|
* been attached. Extracted from `tryUploadPrimitive` so the raw-input path,
|
|
4516
4592
|
* the click-to-surface path, and the CDP native-chooser path all verify the
|
|
4517
|
-
* same way. Returns true as soon as a non-GET capture
|
|
4518
|
-
* {@link UPLOAD_URL_PATTERNS} lands; false if the timeout elapses first.
|
|
4593
|
+
* same way. Returns true as soon as a non-GET capture that is multipart, or is not
|
|
4594
|
+
* known to be body-less and has a URL matching {@link UPLOAD_URL_PATTERNS}, lands; false if the timeout elapses first.
|
|
4519
4595
|
*/
|
|
4520
4596
|
async function waitForUploadNetworkSignal(params) {
|
|
4521
4597
|
const { page, fixture, logger, signalCounter, recentCaptureMeta, stopWhen } = params;
|
|
@@ -4528,6 +4604,10 @@ async function waitForUploadNetworkSignal(params) {
|
|
|
4528
4604
|
const uploadCapture = newCaptures.find((cap) => {
|
|
4529
4605
|
if (cap.method === "GET")
|
|
4530
4606
|
return false;
|
|
4607
|
+
if (cap.multipart)
|
|
4608
|
+
return true;
|
|
4609
|
+
if (cap.hasBody === false)
|
|
4610
|
+
return false;
|
|
4531
4611
|
const lowerUrl = cap.url.toLowerCase();
|
|
4532
4612
|
return UPLOAD_URL_PATTERNS.some((p) => lowerUrl.includes(p));
|
|
4533
4613
|
});
|
|
@@ -4659,8 +4739,9 @@ async function tryUploadPrimitive(params) {
|
|
|
4659
4739
|
* Attach the fixture to an already-surfaced `<input type=file>` (raw or freshly
|
|
4660
4740
|
* mounted after a click) and verify. Extracted verbatim from the original
|
|
4661
4741
|
* `tryUploadPrimitive` body so the raw-input path and the click-to-surface
|
|
4662
|
-
* path share one setInputFiles + framework-change-dispatch + network/DOM verify
|
|
4663
|
-
*
|
|
4742
|
+
* path share one setInputFiles + framework-change-dispatch + network/DOM verify implementation.
|
|
4743
|
+
* Returns true only on a verified signal
|
|
4744
|
+
* (upload POST or populated `.files`).
|
|
4664
4745
|
*/
|
|
4665
4746
|
async function attachToSurfacedInput(params) {
|
|
4666
4747
|
const { page, target, fixture, logger, signalCounter, recentCaptureMeta } = params;
|
|
@@ -4727,18 +4808,10 @@ async function attachToSurfacedInput(params) {
|
|
|
4727
4808
|
.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; })()")
|
|
4728
4809
|
.catch(() => 0);
|
|
4729
4810
|
if (typeof attachedLength !== "number" || attachedLength === 0) {
|
|
4730
|
-
|
|
4731
|
-
//
|
|
4732
|
-
//
|
|
4733
|
-
|
|
4734
|
-
// a DataTransfer fires on the visible drop area — they don't observe
|
|
4735
|
-
// the hidden input's `files[]` mutations even with synthetic `change`
|
|
4736
|
-
// dispatches. This is the documented Playwright community workaround.
|
|
4737
|
-
const dragDropOk = await simulateDragDropUpload(target, fixture, logger);
|
|
4738
|
-
if (dragDropOk) {
|
|
4739
|
-
logger.info(`upload primitive: drag-drop fallback succeeded (name=${fixture.name}, size=${fixture.buffer.length}b)`);
|
|
4740
|
-
return true;
|
|
4741
|
-
}
|
|
4811
|
+
// A dispatched drop is unverified (no network signal, no .files), so it is
|
|
4812
|
+
// not success here: return false and let surfaceAndUpload run the verified
|
|
4813
|
+
// drop and the trusted-click/chooser routes.
|
|
4814
|
+
logger.warn("upload primitive: no network activity within timeout and no file attached in DOM; deferring to surface-and-upload");
|
|
4742
4815
|
return false;
|
|
4743
4816
|
}
|
|
4744
4817
|
logger.info(`upload primitive: file attached in DOM after setInputFiles (deferred-upload widget; name=${fixture.name}, filesLength=${attachedLength})`);
|
|
@@ -5269,30 +5342,108 @@ function segmentedDateStepTokens(text) {
|
|
|
5269
5342
|
return named ? [token] : [];
|
|
5270
5343
|
});
|
|
5271
5344
|
}
|
|
5345
|
+
const SEGMENTED_DATE_STEP_NOISE = new Set([
|
|
5346
|
+
"barnaclevalue",
|
|
5347
|
+
"click",
|
|
5348
|
+
"type",
|
|
5349
|
+
"enter",
|
|
5350
|
+
"fill",
|
|
5351
|
+
"set",
|
|
5352
|
+
"select",
|
|
5353
|
+
"input",
|
|
5354
|
+
"write",
|
|
5355
|
+
"put",
|
|
5356
|
+
"choose",
|
|
5357
|
+
"use",
|
|
5358
|
+
"press",
|
|
5359
|
+
"focus",
|
|
5360
|
+
"spinbutton",
|
|
5361
|
+
"part",
|
|
5362
|
+
"segment",
|
|
5363
|
+
"digits",
|
|
5364
|
+
"box",
|
|
5365
|
+
"first",
|
|
5366
|
+
"second",
|
|
5367
|
+
"into",
|
|
5368
|
+
"as",
|
|
5369
|
+
"by",
|
|
5370
|
+
"from",
|
|
5371
|
+
"to",
|
|
5372
|
+
"is",
|
|
5373
|
+
"be",
|
|
5374
|
+
"it",
|
|
5375
|
+
"its",
|
|
5376
|
+
"this",
|
|
5377
|
+
"that",
|
|
5378
|
+
"my",
|
|
5379
|
+
"your",
|
|
5380
|
+
...SEGMENTED_DATE_STOPWORDS,
|
|
5381
|
+
...SEGMENTED_DATE_FIELD_WORDS,
|
|
5382
|
+
...SEGMENTED_DATE_PART_LABELS,
|
|
5383
|
+
...SEGMENTED_DATE_FIELD_NOUNS,
|
|
5384
|
+
]);
|
|
5272
5385
|
/**
|
|
5273
5386
|
* Pick which spinbutton group a segmented-date step targets. Groups whose
|
|
5274
5387
|
* segment widths (digit count of `aria-valuemax`) don't sum to the value's
|
|
5275
5388
|
* digit count are dropped. A group is only eligible when a field the step names
|
|
5276
5389
|
* (quoted, e.g. 'From', or unquoted in field position, e.g. "the To date")
|
|
5277
5390
|
* matches its OWN label as whole words — a group with no `label` is judged by
|
|
5278
|
-
* its whole `context
|
|
5391
|
+
* its whole `context` — and every other quoted phrase is found in the group's
|
|
5392
|
+
* label or section. The section the step names ("Work Experience 1") is
|
|
5279
5393
|
* then resolved: when a width-fitting group elsewhere matches the step's
|
|
5280
|
-
* section words better than any eligible group does, the
|
|
5281
|
-
*
|
|
5282
|
-
* section
|
|
5283
|
-
*
|
|
5284
|
-
*
|
|
5394
|
+
* section words better than any eligible group does, or the group's own
|
|
5395
|
+
* section shares none of the step's remaining words (or numbering), the named
|
|
5396
|
+
* section renders no such field and the step resolves to nothing rather than to
|
|
5397
|
+
* another section's. Returns the index into `groups`, or `null` when nothing
|
|
5398
|
+
* matches or the best match is tied — writing a date into a group that merely
|
|
5399
|
+
* fits the width would corrupt another field. Groups that hold the same
|
|
5400
|
+
* segments under different names never tie.
|
|
5285
5401
|
*/
|
|
5286
5402
|
function resolveSegmentedDateGroup(instruction, groups) {
|
|
5287
5403
|
const value = parseSegmentedDateValue(instruction);
|
|
5288
5404
|
if (!value)
|
|
5289
5405
|
return null;
|
|
5406
|
+
const compatible = (group) => group.segmentMaxes.map((max) => String(max).length).reduce((a, b) => a + b, 0) ===
|
|
5407
|
+
value.digits.length;
|
|
5408
|
+
return pickSegmentedDateGroup(instruction, groups, value, compatible);
|
|
5409
|
+
}
|
|
5410
|
+
const SEGMENTED_DATE_PART_BY_LABEL = {
|
|
5411
|
+
month: "month",
|
|
5412
|
+
mm: "month",
|
|
5413
|
+
day: "day",
|
|
5414
|
+
dd: "day",
|
|
5415
|
+
year: "year",
|
|
5416
|
+
yyyy: "year",
|
|
5417
|
+
yy: "year",
|
|
5418
|
+
};
|
|
5419
|
+
/**
|
|
5420
|
+
* Parse a per-segment step such as "type '01' into Month". Requires a quoted
|
|
5421
|
+
* 1-4 digit value AND exactly one part label (Month/MM, Day/DD, Year/YYYY), so
|
|
5422
|
+
* a bare short numeric (ZIP fragment, age) is never claimed.
|
|
5423
|
+
*/
|
|
5424
|
+
function parseSegmentedPartStep(instruction) {
|
|
5425
|
+
const raw = parseFillValueIntent(instruction)?.value;
|
|
5426
|
+
if (!raw || !/^\d{1,4}$/.test(raw))
|
|
5427
|
+
return null;
|
|
5428
|
+
const parts = new Set(segmentedDateTokens(instruction.replace(raw, " ")).flatMap((t) => {
|
|
5429
|
+
const part = SEGMENTED_DATE_PART_BY_LABEL[t];
|
|
5430
|
+
return part ? [part] : [];
|
|
5431
|
+
}));
|
|
5432
|
+
const [part] = [...parts];
|
|
5433
|
+
return parts.size === 1 && part ? { digits: raw, valueSpan: raw, part } : null;
|
|
5434
|
+
}
|
|
5435
|
+
/** Index of the single segment in `group` whose own label names `part`, or null when none/ambiguous. */
|
|
5436
|
+
function segmentIndexForPart(group, part) {
|
|
5437
|
+
const hits = (group.segmentLabels ?? []).flatMap((label, si) => segmentedDateTokens(label).some((t) => SEGMENTED_DATE_PART_BY_LABEL[t] === part) ? [si] : []);
|
|
5438
|
+
return hits.length === 1 ? (hits[0] ?? null) : null;
|
|
5439
|
+
}
|
|
5440
|
+
function pickSegmentedDateGroup(instruction, groups, value, compatible) {
|
|
5290
5441
|
const valueTokens = segmentedDateTokens(value.valueSpan).join(" ");
|
|
5291
5442
|
const quoted = (0, quoted_spans_1.findQuotedSpans)(instruction)
|
|
5292
5443
|
.map((s) => segmentedDateTokens(s.text.trim()))
|
|
5293
5444
|
.filter((t) => t.length > 0 &&
|
|
5294
5445
|
t.join(" ") !== valueTokens &&
|
|
5295
|
-
!
|
|
5446
|
+
!t.every((token) => SEGMENTED_DATE_PART_LABELS.has(token)));
|
|
5296
5447
|
const stepTokens = segmentedDateStepTokens(instruction.replace(value.valueSpan, " barnaclevalue "));
|
|
5297
5448
|
const targets = quoted.length > 0 ? quoted : [stepTokens];
|
|
5298
5449
|
const contains = (hay, needle) => {
|
|
@@ -5302,8 +5453,6 @@ function resolveSegmentedDateGroup(instruction, groups) {
|
|
|
5302
5453
|
}
|
|
5303
5454
|
return false;
|
|
5304
5455
|
};
|
|
5305
|
-
const compatible = (group) => group.segmentMaxes.map((max) => String(max).length).reduce((a, b) => a + b, 0) ===
|
|
5306
|
-
value.digits.length;
|
|
5307
5456
|
const stepSet = new Set(stepTokens);
|
|
5308
5457
|
const sectionScore = (group) => {
|
|
5309
5458
|
const own = new Set(segmentedDateTokens(group.label ?? ""));
|
|
@@ -5313,10 +5462,33 @@ function resolveSegmentedDateGroup(instruction, groups) {
|
|
|
5313
5462
|
!SEGMENTED_DATE_STOPWORDS.has(t) &&
|
|
5314
5463
|
!SEGMENTED_DATE_FIELD_WORDS.has(t)).length;
|
|
5315
5464
|
};
|
|
5465
|
+
const quotedTokens = new Set(quoted.flat());
|
|
5466
|
+
const leftover = stepTokens.filter((t) => !SEGMENTED_DATE_STEP_NOISE.has(t) && !quotedTokens.has(t));
|
|
5467
|
+
const isNumber = (t) => /^\d+$/.test(t);
|
|
5468
|
+
// A group under another heading is not the field the step names, however
|
|
5469
|
+
// well its label matches: the step's own words must find the section.
|
|
5470
|
+
// A section that only repeats the group's own label is no heading at all
|
|
5471
|
+
// (Workday enumerates "From*" as both), so it cannot name another section.
|
|
5472
|
+
const namesOtherSection = (group) => {
|
|
5473
|
+
const section = segmentedDateTokens(group.section ?? "");
|
|
5474
|
+
const label = segmentedDateTokens(group.label ?? "");
|
|
5475
|
+
if (section.length === 0 || contains(label, section))
|
|
5476
|
+
return false;
|
|
5477
|
+
const known = new Set([...section, ...label]);
|
|
5478
|
+
const words = leftover.filter((t) => !isNumber(t));
|
|
5479
|
+
const numbers = leftover.filter(isNumber);
|
|
5480
|
+
const sectionNumbers = section.filter(isNumber);
|
|
5481
|
+
if (numbers.length > 0 && sectionNumbers.length > 0) {
|
|
5482
|
+
if (!numbers.some((n) => sectionNumbers.includes(n)))
|
|
5483
|
+
return true;
|
|
5484
|
+
}
|
|
5485
|
+
if (words.some((t) => section.includes(t)))
|
|
5486
|
+
return false;
|
|
5487
|
+
return words.some((t) => !known.has(t));
|
|
5488
|
+
};
|
|
5316
5489
|
const bestSection = Math.max(0, ...groups.filter(compatible).map((group) => sectionScore(group)));
|
|
5317
|
-
let best = -1;
|
|
5318
5490
|
let bestScore = -1;
|
|
5319
|
-
let
|
|
5491
|
+
let leaders = [];
|
|
5320
5492
|
groups.forEach((group, index) => {
|
|
5321
5493
|
if (!compatible(group))
|
|
5322
5494
|
return;
|
|
@@ -5326,24 +5498,43 @@ function resolveSegmentedDateGroup(instruction, groups) {
|
|
|
5326
5498
|
const namesOwnField = group.label === undefined &&
|
|
5327
5499
|
quoted.length === 0 &&
|
|
5328
5500
|
own.some((t) => SEGMENTED_DATE_FIELD_WORDS.has(t) && stepSet.has(t));
|
|
5329
|
-
|
|
5330
|
-
|
|
5501
|
+
const namesOwnLabel = (t) => contains(t, own) || (quoted.length > 0 && contains(own, t));
|
|
5502
|
+
if (!namesOwnField && !targets.some(namesOwnLabel))
|
|
5503
|
+
return;
|
|
5504
|
+
const named = segmentedDateTokens(`${group.section ?? ""} ${group.context}`);
|
|
5505
|
+
if (quoted.some((t) => !namesOwnLabel(t) && !contains(named, t)))
|
|
5506
|
+
return;
|
|
5507
|
+
if (namesOtherSection(group))
|
|
5331
5508
|
return;
|
|
5332
|
-
}
|
|
5333
5509
|
const score = sectionScore(group);
|
|
5334
5510
|
if (score < bestSection)
|
|
5335
5511
|
return;
|
|
5336
5512
|
if (score > bestScore) {
|
|
5337
|
-
best = index;
|
|
5338
5513
|
bestScore = score;
|
|
5339
|
-
|
|
5514
|
+
leaders = [index];
|
|
5340
5515
|
}
|
|
5341
5516
|
else if (score === bestScore) {
|
|
5342
|
-
|
|
5517
|
+
const alias = (a, b) => (groups[a]?.sameAs ?? a) === (groups[b]?.sameAs ?? b) ||
|
|
5518
|
+
groups[a]?.sameAs === b ||
|
|
5519
|
+
groups[b]?.sameAs === a;
|
|
5520
|
+
const twin = leaders.findIndex((leader) => alias(leader, index));
|
|
5521
|
+
if (twin < 0)
|
|
5522
|
+
leaders.push(index);
|
|
5523
|
+
else
|
|
5524
|
+
leaders[twin] = Math.min(leaders[twin] ?? index, index);
|
|
5343
5525
|
}
|
|
5344
5526
|
});
|
|
5345
|
-
if (
|
|
5346
|
-
return
|
|
5527
|
+
if (leaders.length === 1)
|
|
5528
|
+
return leaders[0] ?? null;
|
|
5529
|
+
if (leaders.length > 1) {
|
|
5530
|
+
// A nested group is one wrapper's slice of a larger date widget (e.g. the
|
|
5531
|
+
// Year half of "From MM/YYYY"), so it only ties with a standalone group of
|
|
5532
|
+
// the same label when no section text separates them — Workday renders
|
|
5533
|
+
// "Work Experience 1 From" and "Education 1 From" with no heading the
|
|
5534
|
+
// enumerator can see. The standalone group is the field the step names.
|
|
5535
|
+
const standalone = leaders.filter((i) => !groups[i]?.nested);
|
|
5536
|
+
return standalone.length === 1 ? (standalone[0] ?? null) : null;
|
|
5537
|
+
}
|
|
5347
5538
|
// A step that names no field at all can only mean the one date widget on the
|
|
5348
5539
|
// page; a named field that matched no label must never fall back to it.
|
|
5349
5540
|
if (quoted.length > 0 || stepTokens.some((t) => SEGMENTED_DATE_FIELD_WORDS.has(t)))
|
|
@@ -5351,12 +5542,12 @@ function resolveSegmentedDateGroup(instruction, groups) {
|
|
|
5351
5542
|
const only = groups.flatMap((g, i) => (compatible(g) && !g.nested ? [i] : []));
|
|
5352
5543
|
return only.length === 1 ? (only[0] ?? null) : null;
|
|
5353
5544
|
}
|
|
5354
|
-
|
|
5355
|
-
|
|
5356
|
-
|
|
5357
|
-
|
|
5358
|
-
|
|
5359
|
-
|
|
5545
|
+
/**
|
|
5546
|
+
* In-page helpers shared by the date-segment enumeration and the fill
|
|
5547
|
+
* readback so "is this spinbutton one segment of a multi-segment widget" has a
|
|
5548
|
+
* single definition; defines `spins`, `ownerOf` and `isDateSegment`.
|
|
5549
|
+
*/
|
|
5550
|
+
const SEGMENT_OWNER_JS = `
|
|
5360
5551
|
const hiddenAttr = (node) => node.hasAttribute("hidden") || node.getAttribute("aria-hidden") === "true";
|
|
5361
5552
|
const styleOf = (node) => {
|
|
5362
5553
|
try { return typeof getComputedStyle === "function" ? getComputedStyle(node) : window.getComputedStyle(node); } catch { return null; }
|
|
@@ -5370,18 +5561,53 @@ exports.SEGMENTED_DATE_ENUMERATE_EXPR = `(() => {
|
|
|
5370
5561
|
return false;
|
|
5371
5562
|
};
|
|
5372
5563
|
const spins = Array.from(document.querySelectorAll("input[role='spinbutton']")).filter((el) => !isHidden(el));
|
|
5373
|
-
if (spins.length === 0) return { groups: [] };
|
|
5374
5564
|
const norm = (s) => (s || "").replace(/\\s+/g, " ").trim();
|
|
5375
|
-
const byContainer = new Map();
|
|
5376
|
-
for (const el of spins) {
|
|
5377
|
-
const box = el.closest("fieldset,[role='group']") || el.parentElement;
|
|
5378
|
-
if (!byContainer.has(box)) byContainer.set(box, []);
|
|
5379
|
-
byContainer.get(box).push(el);
|
|
5380
|
-
}
|
|
5381
5565
|
const idText = (ids) => (ids || "").split(/\\s+/).map((id) => {
|
|
5382
5566
|
const ref = id ? document.getElementById(id) : null;
|
|
5383
5567
|
return ref ? norm(ref.textContent) : "";
|
|
5384
5568
|
}).join(" ");
|
|
5569
|
+
const legendOf = (node) => {
|
|
5570
|
+
const legend = node.tagName === "FIELDSET" ? node.querySelector(":scope > legend") : null;
|
|
5571
|
+
return legend ? norm(legend.textContent) : "";
|
|
5572
|
+
};
|
|
5573
|
+
const namedBy = (node) => [legendOf(node), norm(node.getAttribute("aria-label")), idText(node.getAttribute("aria-labelledby"))].filter(Boolean).join(" ");
|
|
5574
|
+
const tiedToSpin = (l) => l.querySelector("input,[role='spinbutton']") || (l.getAttribute("for") && spins.some((sp) => sp.id === l.getAttribute("for")));
|
|
5575
|
+
const hasCaption = (node) => namedBy(node) !== "" || Array.from(node.querySelectorAll("label,legend")).some((l) => !tiedToSpin(l));
|
|
5576
|
+
const captionOwnerOf = (el) => {
|
|
5577
|
+
const explicit = el.closest("fieldset,[role='group']");
|
|
5578
|
+
if (explicit) return explicit;
|
|
5579
|
+
for (let node = el.parentElement; node && node !== document.body; node = node.parentElement) {
|
|
5580
|
+
if (hasCaption(node)) return node;
|
|
5581
|
+
}
|
|
5582
|
+
return null;
|
|
5583
|
+
};
|
|
5584
|
+
const bareSpins = spins.filter((sp) => !captionOwnerOf(sp));
|
|
5585
|
+
const hasOtherControl = (node) => Array.from(node.querySelectorAll("input,select,textarea")).some((c) => !spins.includes(c) && c.type !== "hidden");
|
|
5586
|
+
// Segments that each sit in their own uncaptioned wrapper share no
|
|
5587
|
+
// fieldset/caption, so the nearest ancestor holding several bare spinbuttons
|
|
5588
|
+
// (and no other form control) is the widget they belong to.
|
|
5589
|
+
const bareOwnerOf = (el) => {
|
|
5590
|
+
for (let node = el.parentElement; node && node !== document.body; node = node.parentElement) {
|
|
5591
|
+
if (hasOtherControl(node)) break;
|
|
5592
|
+
if (bareSpins.filter((sp) => node.contains(sp)).length > 1) return node;
|
|
5593
|
+
}
|
|
5594
|
+
return el.parentElement;
|
|
5595
|
+
};
|
|
5596
|
+
const ownerOf = (el) => captionOwnerOf(el) || bareOwnerOf(el);
|
|
5597
|
+
const isDateSegment = (el) => {
|
|
5598
|
+
if (!el.getAttribute || el.getAttribute("role") !== "spinbutton" || !spins.includes(el)) return false;
|
|
5599
|
+
const box = ownerOf(el);
|
|
5600
|
+
return spins.filter((sp) => ownerOf(sp) === box).length > 1;
|
|
5601
|
+
};
|
|
5602
|
+
`;
|
|
5603
|
+
const SEGMENTED_DATE_MARKER_ATTR = "data-barnacle-segdate";
|
|
5604
|
+
/** Exported so tests can run the real DOM enumeration against markup. */
|
|
5605
|
+
exports.SEGMENTED_DATE_ENUMERATE_EXPR = `(() => {
|
|
5606
|
+
const attr = ${JSON.stringify(SEGMENTED_DATE_MARKER_ATTR)};
|
|
5607
|
+
const stale = document.querySelectorAll("[" + attr + "]");
|
|
5608
|
+
stale.forEach((el) => el.removeAttribute(attr));
|
|
5609
|
+
${SEGMENT_OWNER_JS}
|
|
5610
|
+
if (spins.length === 0) return { groups: [] };
|
|
5385
5611
|
const headingSibling = (node) => {
|
|
5386
5612
|
let prevSib = node.previousElementSibling;
|
|
5387
5613
|
for (let hop = 0; hop < 3 && prevSib; hop++, prevSib = prevSib.previousElementSibling) {
|
|
@@ -5389,11 +5615,6 @@ exports.SEGMENTED_DATE_ENUMERATE_EXPR = `(() => {
|
|
|
5389
5615
|
}
|
|
5390
5616
|
return "";
|
|
5391
5617
|
};
|
|
5392
|
-
const legendOf = (node) => {
|
|
5393
|
-
const legend = node.tagName === "FIELDSET" ? node.querySelector(":scope > legend") : null;
|
|
5394
|
-
return legend ? norm(legend.textContent) : "";
|
|
5395
|
-
};
|
|
5396
|
-
const namedBy = (node) => [legendOf(node), norm(node.getAttribute("aria-label")), idText(node.getAttribute("aria-labelledby"))].filter(Boolean).join(" ");
|
|
5397
5618
|
const sectionOf = (box, includeSelf) => {
|
|
5398
5619
|
const parts = [includeSelf ? namedBy(box) : "", headingSibling(box)];
|
|
5399
5620
|
let node = box.parentElement;
|
|
@@ -5408,11 +5629,12 @@ exports.SEGMENTED_DATE_ENUMERATE_EXPR = `(() => {
|
|
|
5408
5629
|
const prev = box.previousElementSibling;
|
|
5409
5630
|
return prev && prev.matches("label,legend") && !prev.querySelector("input,[role='spinbutton']") ? norm(prev.textContent) : "";
|
|
5410
5631
|
};
|
|
5411
|
-
const
|
|
5412
|
-
const
|
|
5413
|
-
const
|
|
5414
|
-
|
|
5415
|
-
|
|
5632
|
+
const byContainer = new Map();
|
|
5633
|
+
for (const el of spins) {
|
|
5634
|
+
const box = ownerOf(el);
|
|
5635
|
+
if (!byContainer.has(box)) byContainer.set(box, []);
|
|
5636
|
+
byContainer.get(box).push(el);
|
|
5637
|
+
}
|
|
5416
5638
|
const labelText = (seg) => {
|
|
5417
5639
|
const parts = [idText(seg.getAttribute("aria-labelledby")), norm(seg.getAttribute("aria-label")), norm(seg.getAttribute("placeholder"))];
|
|
5418
5640
|
if (seg.id) {
|
|
@@ -5441,21 +5663,65 @@ exports.SEGMENTED_DATE_ENUMERATE_EXPR = `(() => {
|
|
|
5441
5663
|
const addGroup = (segs, box, nested) => {
|
|
5442
5664
|
const gi = groups.length;
|
|
5443
5665
|
segs.forEach((seg, si) => mark(seg, gi + ":" + si));
|
|
5444
|
-
|
|
5445
|
-
|
|
5446
|
-
|
|
5447
|
-
|
|
5666
|
+
let inner = null;
|
|
5667
|
+
for (let node = segs[0].parentElement; node && node !== box && box.contains(node) && !inner; node = node.parentElement) {
|
|
5668
|
+
if (segs.every((seg) => node.contains(seg)) && hasCaption(node)) inner = node;
|
|
5669
|
+
}
|
|
5670
|
+
const labelBox = inner || box;
|
|
5671
|
+
const own = namedBy(labelBox) || ownCaption(labelBox) || segs.map((seg) => idText(seg.getAttribute("aria-labelledby"))).filter(Boolean).join(" ");
|
|
5672
|
+
const section = sectionOf(labelBox, Boolean(inner));
|
|
5448
5673
|
groups.push({
|
|
5449
5674
|
segmentMaxes: segs.map((seg, si) => maxOf(seg, si, segs.length)),
|
|
5450
5675
|
context: [section, own].filter(Boolean).join(" "),
|
|
5451
5676
|
label: own,
|
|
5452
5677
|
section,
|
|
5453
5678
|
nested,
|
|
5679
|
+
segmentLabels: segs.map((seg) => labelText(seg)),
|
|
5454
5680
|
});
|
|
5455
5681
|
};
|
|
5682
|
+
const PART_WORD = /^(mm|dd|yyyy|yy|month|day|year)$/i;
|
|
5683
|
+
const keyOf = (text) => norm(text).split(" ").filter((word) => {
|
|
5684
|
+
const parts = word.split(/[^A-Za-z0-9]+/).filter(Boolean);
|
|
5685
|
+
return parts.length > 0 && !parts.every((part) => PART_WORD.test(part));
|
|
5686
|
+
}).join(" ");
|
|
5687
|
+
const ownKey = (seg) => {
|
|
5688
|
+
const parts = [idText(seg.getAttribute("aria-labelledby")), norm(seg.getAttribute("aria-label"))];
|
|
5689
|
+
if (seg.id) {
|
|
5690
|
+
document.querySelectorAll("label").forEach((l) => { if (l.getAttribute("for") === seg.id) parts.push(norm(l.textContent)); });
|
|
5691
|
+
}
|
|
5692
|
+
const wrap = seg.closest("label");
|
|
5693
|
+
if (wrap) parts.push(norm(wrap.textContent));
|
|
5694
|
+
return keyOf(parts.filter(Boolean).join(" "));
|
|
5695
|
+
};
|
|
5696
|
+
// A container's legend names the whole section; the field a segment belongs
|
|
5697
|
+
// to is the caption naming it, or the caption nearest before it with no other
|
|
5698
|
+
// control in between.
|
|
5699
|
+
const fieldKeys = (segs, box) => {
|
|
5700
|
+
const own = new Set(segs);
|
|
5701
|
+
const boxLegend = box.tagName === "FIELDSET" ? box.querySelector(":scope > legend") : null;
|
|
5702
|
+
const keys = new Map();
|
|
5703
|
+
let current = "";
|
|
5704
|
+
box.querySelectorAll("label,legend,input,select,textarea,button").forEach((el) => {
|
|
5705
|
+
if (el === boxLegend) return;
|
|
5706
|
+
if (el.matches("label,legend")) {
|
|
5707
|
+
const target = el.getAttribute("for");
|
|
5708
|
+
if (isHidden(el) || el.querySelector("input,select,textarea,[role='spinbutton']") || (target && box.querySelector("[id='" + target + "']"))) return;
|
|
5709
|
+
current = keyOf(el.textContent);
|
|
5710
|
+
return;
|
|
5711
|
+
}
|
|
5712
|
+
if (!own.has(el)) {
|
|
5713
|
+
if (!isHidden(el)) current = "";
|
|
5714
|
+
return;
|
|
5715
|
+
}
|
|
5716
|
+
keys.set(el, ownKey(el) || current);
|
|
5717
|
+
});
|
|
5718
|
+
return segs.map((seg) => keys.get(seg) || "");
|
|
5719
|
+
};
|
|
5720
|
+
const captionRuns = [];
|
|
5456
5721
|
const nestedCandidates = [];
|
|
5457
5722
|
byContainer.forEach((segs, box) => {
|
|
5458
5723
|
addGroup(segs, box, false);
|
|
5724
|
+
const outerIndex = groups.length - 1;
|
|
5459
5725
|
const byParent = new Map();
|
|
5460
5726
|
for (const seg of segs) {
|
|
5461
5727
|
if (!byParent.has(seg.parentElement)) byParent.set(seg.parentElement, []);
|
|
@@ -5463,9 +5729,36 @@ exports.SEGMENTED_DATE_ENUMERATE_EXPR = `(() => {
|
|
|
5463
5729
|
}
|
|
5464
5730
|
// A shared fieldset can hold several independent dates (From and To year
|
|
5465
5731
|
// spinbuttons side by side); each wrapper is then its own candidate group.
|
|
5466
|
-
if (byParent.size > 1)
|
|
5732
|
+
if (byParent.size > 1) {
|
|
5733
|
+
byParent.forEach((inner, parent) => nestedCandidates.push([inner, parent]));
|
|
5734
|
+
return;
|
|
5735
|
+
}
|
|
5736
|
+
const keys = fieldKeys(segs, box);
|
|
5737
|
+
const runs = [];
|
|
5738
|
+
segs.forEach((seg, si) => {
|
|
5739
|
+
const last = runs[runs.length - 1];
|
|
5740
|
+
if (last && last.key === keys[si]) last.segs.push(seg);
|
|
5741
|
+
else runs.push({ key: keys[si], segs: [seg] });
|
|
5742
|
+
});
|
|
5743
|
+
runs.filter((run) => run.key).forEach((run) => captionRuns.push({ run, box, outerIndex, whole: run.segs.length === segs.length }));
|
|
5467
5744
|
});
|
|
5468
5745
|
nestedCandidates.forEach(([inner, parent]) => addGroup(inner, parent, true));
|
|
5746
|
+
captionRuns.forEach(({ run, box, outerIndex, whole }) => {
|
|
5747
|
+
const outer = groups[outerIndex];
|
|
5748
|
+
const same = (a, b) => a.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim() === b.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
|
|
5749
|
+
if (whole && same(outer.label || "", run.key)) return;
|
|
5750
|
+
const gi = groups.length;
|
|
5751
|
+
run.segs.forEach((seg, si) => mark(seg, gi + ":" + si));
|
|
5752
|
+
const section = sectionOf(box, true);
|
|
5753
|
+
groups.push({
|
|
5754
|
+
segmentMaxes: run.segs.map((seg, si) => maxOf(seg, si, run.segs.length)),
|
|
5755
|
+
context: [section, run.key].filter(Boolean).join(" "),
|
|
5756
|
+
label: run.key,
|
|
5757
|
+
section,
|
|
5758
|
+
nested: true,
|
|
5759
|
+
sameAs: whole ? outerIndex : undefined,
|
|
5760
|
+
});
|
|
5761
|
+
});
|
|
5469
5762
|
return { groups };
|
|
5470
5763
|
})()`;
|
|
5471
5764
|
const SEGMENTED_DATE_CLEAR_EXPR = `(() => {
|
|
@@ -5484,15 +5777,29 @@ const SEGMENTED_DATE_CLEAR_EXPR = `(() => {
|
|
|
5484
5777
|
*/
|
|
5485
5778
|
async function trySegmentedDateSpinbuttonPrimitive(params) {
|
|
5486
5779
|
const { page, target, instruction, logger } = params;
|
|
5487
|
-
const
|
|
5488
|
-
|
|
5780
|
+
const whole = parseSegmentedDateValue(instruction);
|
|
5781
|
+
const part = parseSegmentedPartStep(instruction);
|
|
5782
|
+
if (!whole && !part)
|
|
5489
5783
|
return null;
|
|
5490
|
-
const sendCDP = (
|
|
5784
|
+
const sendCDP = frameCdpSender(page, target);
|
|
5491
5785
|
try {
|
|
5492
5786
|
const enumerated = (await target.evaluate(exports.SEGMENTED_DATE_ENUMERATE_EXPR));
|
|
5493
5787
|
if (!enumerated?.groups?.length)
|
|
5494
5788
|
return null;
|
|
5495
|
-
const
|
|
5789
|
+
const wholeGi = whole ? resolveSegmentedDateGroup(instruction, enumerated.groups) : null;
|
|
5790
|
+
const partStep = wholeGi === null ? part : null;
|
|
5791
|
+
const value = (wholeGi !== null ? whole : part);
|
|
5792
|
+
const partOf = (group) => partStep ? segmentIndexForPart(group, partStep.part) : null;
|
|
5793
|
+
const partFits = (group) => {
|
|
5794
|
+
const si = partOf(group);
|
|
5795
|
+
return (!group.nested &&
|
|
5796
|
+
si !== null &&
|
|
5797
|
+
value.digits.length <= String(group.segmentMaxes[si] ?? 0).length);
|
|
5798
|
+
};
|
|
5799
|
+
const gi = wholeGi ??
|
|
5800
|
+
(partStep
|
|
5801
|
+
? pickSegmentedDateGroup(instruction, enumerated.groups, partStep, partFits)
|
|
5802
|
+
: null);
|
|
5496
5803
|
if (gi === null) {
|
|
5497
5804
|
logger.info("segmented date primitive: no unambiguous group; falling through");
|
|
5498
5805
|
await target.evaluate(SEGMENTED_DATE_CLEAR_EXPR);
|
|
@@ -5500,10 +5807,15 @@ async function trySegmentedDateSpinbuttonPrimitive(params) {
|
|
|
5500
5807
|
}
|
|
5501
5808
|
const maxes = enumerated.groups[gi]?.segmentMaxes ?? [];
|
|
5502
5809
|
const widths = maxes.map((max) => String(max).length);
|
|
5503
|
-
const
|
|
5504
|
-
|
|
5505
|
-
|
|
5506
|
-
|
|
5810
|
+
const onlySegment = partStep ? partOf(enumerated.groups[gi]) : null;
|
|
5811
|
+
const active = widths.flatMap((_, si) => onlySegment === null || si === onlySegment ? [si] : []);
|
|
5812
|
+
const sliceFor = (si) => {
|
|
5813
|
+
if (onlySegment !== null)
|
|
5814
|
+
return value.digits;
|
|
5815
|
+
const start = widths.slice(0, si).reduce((a, b) => a + b, 0);
|
|
5816
|
+
return value.digits.slice(start, start + (widths[si] ?? 0));
|
|
5817
|
+
};
|
|
5818
|
+
const expected = active.map((si) => Number(sliceFor(si)));
|
|
5507
5819
|
const segmentSelector = (si) => `[${SEGMENTED_DATE_MARKER_ATTR}~="${gi}:${si}"]`;
|
|
5508
5820
|
const focusSegment = async (si) => {
|
|
5509
5821
|
const selector = segmentSelector(si);
|
|
@@ -5516,7 +5828,9 @@ async function trySegmentedDateSpinbuttonPrimitive(params) {
|
|
|
5516
5828
|
// scrolling is best-effort; the key events only need the segment focused
|
|
5517
5829
|
}
|
|
5518
5830
|
try {
|
|
5519
|
-
|
|
5831
|
+
// An open option popup can cover the field; clear it so the trusted
|
|
5832
|
+
// click lands on the segment instead of falling back to a bare focus().
|
|
5833
|
+
if (await clearObscuringPopup(page, target, selector)) {
|
|
5520
5834
|
await (0, trusted_click_1.clickCentredTrusted)(target, selector, { label: "date segment" });
|
|
5521
5835
|
}
|
|
5522
5836
|
}
|
|
@@ -5536,12 +5850,9 @@ async function trySegmentedDateSpinbuttonPrimitive(params) {
|
|
|
5536
5850
|
await sendCDP("Input.dispatchKeyEvent", { type: "keyUp", ...keyArgs });
|
|
5537
5851
|
}
|
|
5538
5852
|
};
|
|
5539
|
-
const segmentDigits =
|
|
5540
|
-
const from = widths.slice(0, i).reduce((acc, n) => acc + n, 0);
|
|
5541
|
-
return value.digits.slice(from, from + w);
|
|
5542
|
-
});
|
|
5853
|
+
const segmentDigits = active.map((si) => sliceFor(si));
|
|
5543
5854
|
const readSegments = async () => (await target.evaluate(`(() => {
|
|
5544
|
-
const sels = ${JSON.stringify(
|
|
5855
|
+
const sels = ${JSON.stringify(active.map((si) => segmentSelector(si)))};
|
|
5545
5856
|
return sels.map((sel) => {
|
|
5546
5857
|
const s = document.querySelector(sel);
|
|
5547
5858
|
if (!s) return NaN;
|
|
@@ -5558,15 +5869,18 @@ async function trySegmentedDateSpinbuttonPrimitive(params) {
|
|
|
5558
5869
|
// locator.type (per-character key events, never Input.insertText) covers a
|
|
5559
5870
|
// page whose CDP channel does not reach the widget.
|
|
5560
5871
|
const typeViaLocator = async () => {
|
|
5561
|
-
for (const [
|
|
5562
|
-
await target
|
|
5872
|
+
for (const [k, digits] of segmentDigits.entries()) {
|
|
5873
|
+
await target
|
|
5874
|
+
.locator(segmentSelector(active[k] ?? 0))
|
|
5875
|
+
.first()
|
|
5876
|
+
.type(digits, {
|
|
5563
5877
|
delay: SEGMENTED_DATE_KEY_DELAY_MS,
|
|
5564
5878
|
});
|
|
5565
5879
|
}
|
|
5566
5880
|
};
|
|
5567
5881
|
const typeViaKeyEvents = async () => {
|
|
5568
|
-
for (const [
|
|
5569
|
-
await focusSegment(
|
|
5882
|
+
for (const [k, digits] of segmentDigits.entries()) {
|
|
5883
|
+
await focusSegment(active[k] ?? 0);
|
|
5570
5884
|
await typeViaCdp(digits);
|
|
5571
5885
|
}
|
|
5572
5886
|
};
|
|
@@ -5961,6 +6275,16 @@ function isNavigatingEvaluateRejection(error) {
|
|
|
5961
6275
|
message.includes("no frame for given id found") ||
|
|
5962
6276
|
GENERIC_CONTENT_FREE_EVAL_REJECTION_MESSAGES.has(message.trim()));
|
|
5963
6277
|
}
|
|
6278
|
+
/**
|
|
6279
|
+
* Settles after a click and returns the post-click snapshot, so the settle
|
|
6280
|
+
* wait and the phantom classifier decide from the same reader and baseline.
|
|
6281
|
+
* Without a page there is nothing to wait on and the snapshot is taken as-is.
|
|
6282
|
+
*/
|
|
6283
|
+
async function settleAndSnapshotPostClick(target, signalCounter, page, pre, midFlowSubmit = false) {
|
|
6284
|
+
if (page)
|
|
6285
|
+
await settleWithPageQuiescence(page, target, signalCounter, pre.networkCount, true, pre.visibleTextSignature, pageAdvancedSince(pre.visibleTextSignature, midFlowSubmit));
|
|
6286
|
+
return snapshotPage(target, signalCounter, page);
|
|
6287
|
+
}
|
|
5964
6288
|
/**
|
|
5965
6289
|
* Shared in-page discovery of the widget's callback, used identically by
|
|
5966
6290
|
* `injectCaptchaTokenAndSubmit`'s precheck (to decide the branch) and its
|
|
@@ -6268,9 +6592,7 @@ async function submitCaptchaGatedForm(target, responseField = "h-captcha-respons
|
|
|
6268
6592
|
if (clickResult.clicked) {
|
|
6269
6593
|
if (!pre || !verification)
|
|
6270
6594
|
return true;
|
|
6271
|
-
|
|
6272
|
-
await (0, click_settle_1.settleAfterClick)(verification.page, verification.signalCounter, pre.networkCount, true, async () => (await snapshotPage(target, verification.signalCounter)).visibleTextSignature, pre.visibleTextSignature);
|
|
6273
|
-
const post = await snapshotPage(target, verification.signalCounter, verification.page);
|
|
6595
|
+
const post = await settleAndSnapshotPostClick(target, verification.signalCounter, verification.page, pre, verification.isMidFlowSubmitStep);
|
|
6274
6596
|
const verdict = (0, phantom_click_1.classifyPhantomClick)({
|
|
6275
6597
|
actResultSuccess: true,
|
|
6276
6598
|
pre,
|
|
@@ -6291,9 +6613,7 @@ async function submitCaptchaGatedForm(target, responseField = "h-captcha-respons
|
|
|
6291
6613
|
throw err;
|
|
6292
6614
|
});
|
|
6293
6615
|
if (runnerUpClickResult.clicked) {
|
|
6294
|
-
|
|
6295
|
-
await (0, click_settle_1.settleAfterClick)(verification.page, verification.signalCounter, post.networkCount, true, async () => (await snapshotPage(target, verification.signalCounter)).visibleTextSignature, post.visibleTextSignature);
|
|
6296
|
-
const runnerUpPost = await snapshotPage(target, verification.signalCounter, verification.page);
|
|
6616
|
+
const runnerUpPost = await settleAndSnapshotPostClick(target, verification.signalCounter, verification.page, post, verification.isMidFlowSubmitStep);
|
|
6297
6617
|
const runnerUpVerdict = (0, phantom_click_1.classifyPhantomClick)({
|
|
6298
6618
|
actResultSuccess: true,
|
|
6299
6619
|
pre: post,
|
|
@@ -6839,7 +7159,10 @@ async function tryCheckboxStatePrimitive(params) {
|
|
|
6839
7159
|
let overlaid = false;
|
|
6840
7160
|
let trustedDelivered = false;
|
|
6841
7161
|
for (const sel of clickSels) {
|
|
6842
|
-
const
|
|
7162
|
+
const firstProbe = await (0, trusted_click_1.probeClickPoint)(target, sel);
|
|
7163
|
+
const probe = firstProbe?.state === "occluded" && firstProbe.occluder !== "none"
|
|
7164
|
+
? await dismissLeftoverPromptPopups(page, target).then(() => (0, trusted_click_1.probeClickPoint)(target, sel), () => firstProbe)
|
|
7165
|
+
: firstProbe;
|
|
6843
7166
|
if (probe?.state === "occluded" && probe.occluder !== "none") {
|
|
6844
7167
|
overlaid = true;
|
|
6845
7168
|
continue;
|
|
@@ -7897,7 +8220,7 @@ async function tryPromptSelectorPrimitive(params) {
|
|
|
7897
8220
|
logger.info(`prompt-selector primitive: filtered list for ${optLabel} names no matching option; submitting with Enter`);
|
|
7898
8221
|
try {
|
|
7899
8222
|
await target.evaluate(`(() => { const el = document.querySelector('[${PROMPT_OPTION_MARK_ATTR}-search="1"]'); if (el) el.focus(); })()`);
|
|
7900
|
-
await sendTrustedEnter(page);
|
|
8223
|
+
await sendTrustedEnter(page, target);
|
|
7901
8224
|
}
|
|
7902
8225
|
catch (err) {
|
|
7903
8226
|
logger.info(`prompt-selector primitive: enter submit failed for ${optLabel}: ${(0, errors_1.toErrorMessage)(err)}`);
|
|
@@ -7940,7 +8263,7 @@ const PROMPT_SELECTOR_MAX_STABLE_READS = 5;
|
|
|
7940
8263
|
* committer is never handed an unsettled list.
|
|
7941
8264
|
*/
|
|
7942
8265
|
async function readStablePromptOptions(page, target, enumerateOptionsExpr, initial) {
|
|
7943
|
-
const key = (r) => JSON.stringify((r?.options ?? []).map((o) => [o.text, o.
|
|
8266
|
+
const key = (r) => JSON.stringify((r?.options ?? []).map((o) => [o.text, o.domText ?? null]));
|
|
7944
8267
|
let current = initial;
|
|
7945
8268
|
for (let read = 0; read < PROMPT_SELECTOR_MAX_STABLE_READS; read++) {
|
|
7946
8269
|
await page.waitForTimeout(PROMPT_SELECTOR_SETTLE_MS);
|
|
@@ -7979,33 +8302,78 @@ function promptOptionsNameOption(result, option) {
|
|
|
7979
8302
|
const want = normPromptOptionText(option);
|
|
7980
8303
|
if (want === "")
|
|
7981
8304
|
return true;
|
|
7982
|
-
return (result?.options ?? []).some((o) => {
|
|
8305
|
+
return realPromptOptions(result?.options ?? [], option).some((o) => {
|
|
8306
|
+
const text = normPromptOptionText(o.text);
|
|
8307
|
+
return text === want || text.includes(want);
|
|
8308
|
+
});
|
|
8309
|
+
}
|
|
8310
|
+
/**
|
|
8311
|
+
* Single classification of which rendered entries are real options, shared by
|
|
8312
|
+
* the submit decision and the committer so they cannot disagree. Placeholders
|
|
8313
|
+
* ("No Items.", "No results for x") are dropped, except an entry that is
|
|
8314
|
+
* exactly the requested text, which is the user's own option.
|
|
8315
|
+
*/
|
|
8316
|
+
function realPromptOptions(options, option) {
|
|
8317
|
+
const want = normPromptOptionText(option);
|
|
8318
|
+
return options.filter((o) => {
|
|
7983
8319
|
const text = normPromptOptionText(o.text);
|
|
7984
8320
|
if (text === "")
|
|
7985
8321
|
return false;
|
|
7986
|
-
|
|
7987
|
-
return true;
|
|
7988
|
-
if (PROMPT_PLACEHOLDER_RE.test(text))
|
|
7989
|
-
return false;
|
|
7990
|
-
return text.includes(want);
|
|
8322
|
+
return text === want || !PROMPT_PLACEHOLDER_RE.test(text);
|
|
7991
8323
|
});
|
|
7992
8324
|
}
|
|
7993
|
-
/**
|
|
7994
|
-
|
|
7995
|
-
|
|
7996
|
-
|
|
7997
|
-
|
|
8325
|
+
/**
|
|
8326
|
+
* Single owner of CDP delivery for trusted input: a child-frame target's own
|
|
8327
|
+
* session when it has one, otherwise the page's, so keys reach the frame that
|
|
8328
|
+
* holds the focused element.
|
|
8329
|
+
*/
|
|
8330
|
+
function frameCdpSender(page, target) {
|
|
8331
|
+
return (method, args) => target.frame?.session ? target.frame.session.send(method, args) : page.sendCDP(method, args);
|
|
7998
8332
|
}
|
|
7999
|
-
/**
|
|
8000
|
-
|
|
8001
|
-
|
|
8333
|
+
/**
|
|
8334
|
+
* Clicks at a frame-local viewport point through the same CDP route as keys, so
|
|
8335
|
+
* the press lands where the hit test just ran instead of re-resolving a locator.
|
|
8336
|
+
* A same-process child frame has no session of its own, so its offset in the
|
|
8337
|
+
* page is added to reach page coordinates.
|
|
8338
|
+
*/
|
|
8339
|
+
async function dispatchTrustedClick(page, target, x, y) {
|
|
8340
|
+
if (target.frame === null) {
|
|
8341
|
+
await page.click(x, y);
|
|
8342
|
+
return;
|
|
8343
|
+
}
|
|
8344
|
+
const send = frameCdpSender(page, target);
|
|
8345
|
+
const offset = target.frame.session
|
|
8346
|
+
? { x: 0, y: 0 }
|
|
8347
|
+
: ((await target
|
|
8348
|
+
.evaluate(`(() => {
|
|
8349
|
+
const f = window.frameElement;
|
|
8350
|
+
if (!f) return null;
|
|
8351
|
+
const r = f.getBoundingClientRect();
|
|
8352
|
+
return { x: r.left, y: r.top };
|
|
8353
|
+
})()`)
|
|
8354
|
+
.catch(() => null)) ?? { x: 0, y: 0 });
|
|
8355
|
+
const point = { x: x + offset.x, y: y + offset.y, button: "left", clickCount: 1 };
|
|
8356
|
+
await send("Input.dispatchMouseEvent", { type: "mouseMoved", x: point.x, y: point.y });
|
|
8357
|
+
await send("Input.dispatchMouseEvent", { type: "mousePressed", ...point });
|
|
8358
|
+
await send("Input.dispatchMouseEvent", { type: "mouseReleased", ...point });
|
|
8359
|
+
}
|
|
8360
|
+
async function sendTrustedKey(page, target, key, text) {
|
|
8361
|
+
const send = frameCdpSender(page, target);
|
|
8362
|
+
await send("Input.dispatchKeyEvent", { type: "keyDown", ...(text ? { text } : {}), ...key });
|
|
8363
|
+
await send("Input.dispatchKeyEvent", { type: "keyUp", ...key });
|
|
8364
|
+
}
|
|
8365
|
+
/** Dispatches a trusted Enter to the focused element (the filter input) via the target frame's CDP session. */
|
|
8366
|
+
async function sendTrustedEnter(page, target) {
|
|
8367
|
+
await sendTrustedKey(page, target, { key: "Enter", code: "Enter", windowsVirtualKeyCode: 13, nativeVirtualKeyCode: 13 }, "\r");
|
|
8368
|
+
}
|
|
8369
|
+
/** Dispatches a trusted Escape to the focused element via the target frame's CDP session, closing a popup left open. */
|
|
8370
|
+
async function sendTrustedEscape(page, target) {
|
|
8371
|
+
await sendTrustedKey(page, target, {
|
|
8002
8372
|
key: "Escape",
|
|
8003
8373
|
code: "Escape",
|
|
8004
8374
|
windowsVirtualKeyCode: 27,
|
|
8005
8375
|
nativeVirtualKeyCode: 27,
|
|
8006
|
-
};
|
|
8007
|
-
await page.sendCDP("Input.dispatchKeyEvent", { type: "keyDown", ...key });
|
|
8008
|
-
await page.sendCDP("Input.dispatchKeyEvent", { type: "keyUp", ...key });
|
|
8376
|
+
});
|
|
8009
8377
|
}
|
|
8010
8378
|
/**
|
|
8011
8379
|
* Counts rendered listbox / activeListContainer popups other than the chip
|
|
@@ -8049,18 +8417,52 @@ async function dismissLeftoverPromptPopups(page, target) {
|
|
|
8049
8417
|
.catch(() => 0));
|
|
8050
8418
|
if (!count)
|
|
8051
8419
|
return;
|
|
8052
|
-
logger.info(`
|
|
8053
|
-
const trusted = await sendTrustedEscape(page)
|
|
8420
|
+
logger.info(`popup dismissal: dismissing ${count} leftover popup(s) via Escape`);
|
|
8421
|
+
const trusted = await sendTrustedEscape(page, target)
|
|
8054
8422
|
.then(() => true)
|
|
8055
8423
|
.catch((err) => {
|
|
8056
|
-
logger.info(`
|
|
8424
|
+
logger.info(`popup dismissal: Escape dispatch failed: ${(0, errors_1.toErrorMessage)(err)}`);
|
|
8057
8425
|
return false;
|
|
8058
8426
|
});
|
|
8059
8427
|
if (!trusted || attempt > 0)
|
|
8060
8428
|
await target.evaluate(IN_PAGE_ESCAPE_EXPR).catch(() => undefined);
|
|
8061
|
-
await page
|
|
8429
|
+
await settleAfterEscape(page);
|
|
8062
8430
|
}
|
|
8063
8431
|
}
|
|
8432
|
+
/** Waits for a dismissed popup to unpaint; a page without a timer seam just proceeds. */
|
|
8433
|
+
async function settleAfterEscape(page) {
|
|
8434
|
+
await Promise.resolve()
|
|
8435
|
+
.then(() => page.waitForTimeout(PROMPT_SELECTOR_SETTLE_MS))
|
|
8436
|
+
.catch(() => undefined);
|
|
8437
|
+
}
|
|
8438
|
+
/**
|
|
8439
|
+
* Clears whatever popup paints over `selector`'s click point so the caller's
|
|
8440
|
+
* trusted click lands on the field. The hit test, not the popup's markup, is
|
|
8441
|
+
* the evidence: a named occluder gets the listbox-aware dismissal first and
|
|
8442
|
+
* then a plain Escape for popups that dismissal does not recognise. Resolves
|
|
8443
|
+
* true only when a fresh hit test reaches the field; an indeterminate probe
|
|
8444
|
+
* (no layout, nothing at the point) only ever gets the listbox-aware
|
|
8445
|
+
* dismissal, never a blind Escape.
|
|
8446
|
+
*/
|
|
8447
|
+
async function clearObscuringPopup(page, target, selector) {
|
|
8448
|
+
const covered = (probe) => probe?.state === "occluded" && probe.occluder !== undefined && probe.occluder !== "none";
|
|
8449
|
+
const first = await (0, trusted_click_1.probeClickPoint)(target, selector);
|
|
8450
|
+
if (first?.state === "hit")
|
|
8451
|
+
return true;
|
|
8452
|
+
await dismissLeftoverPromptPopups(page, target).catch(() => undefined);
|
|
8453
|
+
const afterListbox = await (0, trusted_click_1.probeClickPoint)(target, selector);
|
|
8454
|
+
if (afterListbox?.state === "hit")
|
|
8455
|
+
return true;
|
|
8456
|
+
if (!covered(afterListbox))
|
|
8457
|
+
return false;
|
|
8458
|
+
const trusted = await sendTrustedEscape(page, target)
|
|
8459
|
+
.then(() => true)
|
|
8460
|
+
.catch(() => false);
|
|
8461
|
+
if (!trusted)
|
|
8462
|
+
await target.evaluate(IN_PAGE_ESCAPE_EXPR).catch(() => undefined);
|
|
8463
|
+
await settleAfterEscape(page);
|
|
8464
|
+
return (await (0, trusted_click_1.probeClickPoint)(target, selector))?.state === "hit";
|
|
8465
|
+
}
|
|
8064
8466
|
/**
|
|
8065
8467
|
* Phase 3 of {@link tryPromptSelectorPrimitive}: given the popup's rendered
|
|
8066
8468
|
* (possibly filtered) option list, match the requested option — deterministic
|
|
@@ -8091,7 +8493,7 @@ async function commitPromptOption(params) {
|
|
|
8091
8493
|
.join("");
|
|
8092
8494
|
let optionsResult = params.optionsResult;
|
|
8093
8495
|
for (let depth = 0; depth <= PROMPT_SELECTOR_MAX_DRILL_DEPTH; depth++) {
|
|
8094
|
-
const options = optionsResult.options ?? [];
|
|
8496
|
+
const options = realPromptOptions(optionsResult.options ?? [], option);
|
|
8095
8497
|
const detMatch = options.find((o) => norm(o.text) === wantOpt) ?? null;
|
|
8096
8498
|
if (detMatch === null && (anthropic === null || options.length === 0)) {
|
|
8097
8499
|
logger.info(`prompt-selector primitive: no option match for ${optLabel}${anthropic === null ? " (no LLM client)" : ""}; falling through to cascade`);
|
|
@@ -8129,7 +8531,7 @@ async function commitPromptOption(params) {
|
|
|
8129
8531
|
let clicked = false;
|
|
8130
8532
|
for (let attempt = 0; attempt < PROMPT_SELECTOR_MAX_CLICK_RESOLVES && !clicked; attempt++) {
|
|
8131
8533
|
const fresh = (await target.evaluate(enumerateOptionsExpr).catch(() => null));
|
|
8132
|
-
const freshMatches = (fresh?.options ?? []).filter((o) => norm(o.text) === wantChosenText);
|
|
8534
|
+
const freshMatches = realPromptOptions(fresh?.options ?? [], option).filter((o) => norm(o.text) === wantChosenText);
|
|
8133
8535
|
const freshOption = freshMatches.find((o) => o.nodeId !== undefined && o.nodeId === chosenOption.nodeId) ??
|
|
8134
8536
|
freshMatches[0];
|
|
8135
8537
|
if (!freshOption) {
|
|
@@ -8152,6 +8554,19 @@ async function commitPromptOption(params) {
|
|
|
8152
8554
|
await (0, trusted_click_1.clickCentredTrusted)(target, optionSel, {
|
|
8153
8555
|
label: "prompt-selector option",
|
|
8154
8556
|
expectText: expectedDomText,
|
|
8557
|
+
clickAtPoint: async (x, y) => {
|
|
8558
|
+
const stillThere = await target
|
|
8559
|
+
.evaluate(`((x, y, want) => {
|
|
8560
|
+
const hit = document.elementFromPoint(x, y);
|
|
8561
|
+
const el = hit && hit.closest(${JSON.stringify(optionSel)});
|
|
8562
|
+
return !!el && (el.textContent || "").replace(/\\s+/g, " ").trim() === want;
|
|
8563
|
+
})(${x}, ${y}, ${JSON.stringify(expectedDomText)})`)
|
|
8564
|
+
.catch(() => false);
|
|
8565
|
+
if (!stillThere) {
|
|
8566
|
+
throw new trusted_click_1.CentredClickError("centred click: prompt-selector option identity mismatch at click point, click withheld", "mismatch");
|
|
8567
|
+
}
|
|
8568
|
+
await dispatchTrustedClick(page, target, x, y);
|
|
8569
|
+
},
|
|
8155
8570
|
verifyBeforeClick: async () => (await target
|
|
8156
8571
|
.evaluate(`((sel, want) => {
|
|
8157
8572
|
const el = document.querySelector(sel);
|
|
@@ -8180,6 +8595,7 @@ async function commitPromptOption(params) {
|
|
|
8180
8595
|
const emptyRx = new RegExp(emptyRxSrc, emptyRxFlags);
|
|
8181
8596
|
const currentText = ${PROMPT_CURRENT_TEXT_EXPR};
|
|
8182
8597
|
const announcedValue = ${PROMPT_ANNOUNCED_VALUE_EXPR};
|
|
8598
|
+
const namesOption = ${PROMPT_NAMES_OPTION_EXPR};
|
|
8183
8599
|
const w = document.querySelector("[" + markAttr + '="' + wIdx + '"]');
|
|
8184
8600
|
if (!w) return { ok: false, id: "", stillInvalid: false };
|
|
8185
8601
|
const text = currentText(w, valueSel, emptyRx);
|
|
@@ -8238,7 +8654,7 @@ async function commitPromptOption(params) {
|
|
|
8238
8654
|
// The widget's own announcement of what it committed is ground truth when readable.
|
|
8239
8655
|
const committed = announcedValue(w);
|
|
8240
8656
|
if (committed) {
|
|
8241
|
-
return { ok: !!wantText && committed
|
|
8657
|
+
return { ok: !!wantText && namesOption(committed, wantText), id: w.id || "", stillInvalid, committed };
|
|
8242
8658
|
}
|
|
8243
8659
|
// A wrong option leaves the widget valid, so !stillInvalid proves nothing
|
|
8244
8660
|
// once the widget's own value is readable: a readable value naming another
|
|
@@ -8410,6 +8826,9 @@ async function hasUnfilledRequiredControlForStep(target, instruction) {
|
|
|
8410
8826
|
* upload/dropzone/file-related class or tag names. Tries multiple
|
|
8411
8827
|
* candidates in order. Returns true on first success.
|
|
8412
8828
|
*
|
|
8829
|
+
* Dispatch-only: a true return means the drop events fired, not that the
|
|
8830
|
+
* widget accepted the file; callers must verify.
|
|
8831
|
+
*
|
|
8413
8832
|
* Site-agnostic — DataTransfer + DragEvent dispatch is universal HTML5
|
|
8414
8833
|
* drag-and-drop API. Works on react-dropzone, Material Dropzone, custom
|
|
8415
8834
|
* <uapp-upload>/<app-upload>, and any other drop-zone-based upload UI.
|
|
@@ -9517,11 +9936,7 @@ async function probeChildFrameSubmitFallback(params) {
|
|
|
9517
9936
|
const clickResult = (await Promise.resolve(target.evaluate((0, submit_control_1.buildClickByDeepIndexExpr)(top.deepIndex))).catch(() => ({ clicked: false })));
|
|
9518
9937
|
if (!clickResult.clicked)
|
|
9519
9938
|
continue;
|
|
9520
|
-
await (
|
|
9521
|
-
const snap = await snapshotPage(target, signalCounter);
|
|
9522
|
-
return snap.visibleTextSignature;
|
|
9523
|
-
}, pre.visibleTextSignature);
|
|
9524
|
-
const post = await snapshotPage(target, signalCounter, page);
|
|
9939
|
+
const post = await settleAndSnapshotPostClick(target, signalCounter, page, pre, isMidFlowSubmitStep);
|
|
9525
9940
|
const verdict = (0, phantom_click_1.classifyPhantomClick)({
|
|
9526
9941
|
actResultSuccess: true,
|
|
9527
9942
|
pre,
|
|
@@ -9538,11 +9953,7 @@ async function probeChildFrameSubmitFallback(params) {
|
|
|
9538
9953
|
const runnerUpClickResult = (await Promise.resolve(target.evaluate((0, submit_control_1.buildClickByDeepIndexExpr)(runnerUp.deepIndex))).catch(() => ({ clicked: false })));
|
|
9539
9954
|
if (!runnerUpClickResult.clicked)
|
|
9540
9955
|
continue;
|
|
9541
|
-
await (
|
|
9542
|
-
const snap = await snapshotPage(target, signalCounter);
|
|
9543
|
-
return snap.visibleTextSignature;
|
|
9544
|
-
}, post.visibleTextSignature);
|
|
9545
|
-
const runnerUpPost = await snapshotPage(target, signalCounter, page);
|
|
9956
|
+
const runnerUpPost = await settleAndSnapshotPostClick(target, signalCounter, page, post, isMidFlowSubmitStep);
|
|
9546
9957
|
const runnerUpVerdict = (0, phantom_click_1.classifyPhantomClick)({
|
|
9547
9958
|
actResultSuccess: true,
|
|
9548
9959
|
pre,
|
|
@@ -10299,7 +10710,7 @@ async function executeStepWithHealing(params) {
|
|
|
10299
10710
|
});
|
|
10300
10711
|
if (backendErrorUrl !== null) {
|
|
10301
10712
|
logger.error(`${formatStepPrefix(stepIndex, totalSteps)} backend error detected (submit endpoint returned 5xx: ${backendErrorUrl}); aborting cascade`);
|
|
10302
|
-
throw new errors_2.StepVerificationError(`${formatStepPrefix(stepIndex, totalSteps)} (${(0, pii_redaction_1.
|
|
10713
|
+
throw new errors_2.StepVerificationError(`${formatStepPrefix(stepIndex, totalSteps)} (${(0, pii_redaction_1.redactPii)(step).slice(0, 60)}) backend 5xx at ${backendErrorUrl} — unrecoverable`, "backend-error-unrecoverable");
|
|
10303
10714
|
}
|
|
10304
10715
|
// Bounded same-origin child-iframe fallback: the probe above only ever
|
|
10305
10716
|
// looked at the resolved (declared-or-main) frame's own light+shadow DOM,
|
|
@@ -10384,7 +10795,7 @@ async function executeStepWithHealing(params) {
|
|
|
10384
10795
|
targetResolutionDiagnostic,
|
|
10385
10796
|
fieldValuesAtFailure,
|
|
10386
10797
|
}) ?? null;
|
|
10387
|
-
throw new errors_2.StepVerificationError(`${formatStepPrefix(stepIndex, totalSteps)} (${(0, pii_redaction_1.
|
|
10798
|
+
throw new errors_2.StepVerificationError(`${formatStepPrefix(stepIndex, totalSteps)} (${(0, pii_redaction_1.redactPii)(step).slice(0, 60)}) probe found no candidates on page${dumpPath ? `; see ${dumpPath}` : ""}`, "probe-absent");
|
|
10388
10799
|
}
|
|
10389
10800
|
// Pre-submit form-validity probe. Fires on the canonical submit step
|
|
10390
10801
|
// (either the final flow step OR a step explicitly flagged `submitStep:
|
|
@@ -10766,13 +11177,13 @@ async function executeStepWithHealing(params) {
|
|
|
10766
11177
|
// control stays covered so the attempt fails instead of clicking an overlay.
|
|
10767
11178
|
// Resolves true once the observe ran, so guardedAct skips its own pass.
|
|
10768
11179
|
const preCentreNaturalLanguageClick = async (instruction) => {
|
|
10769
|
-
if (!stagehand_guard_1.
|
|
11180
|
+
if (!(0, stagehand_guard_1.hasClickIntent)(instruction))
|
|
10770
11181
|
return false;
|
|
10771
|
-
const observed = await (0, stagehand_guard_1.guardedObserve)(stagehand, instruction, { timeout: exports.STEP_WATCHDOG_MS }, captureFn, frameTarget)
|
|
11182
|
+
const observed = await (0, stagehand_guard_1.guardedObserve)(stagehand, instruction, { timeout: exports.STEP_WATCHDOG_MS }, captureFn, frameTarget);
|
|
10772
11183
|
const first = observed[0];
|
|
10773
|
-
if (first?.method
|
|
11184
|
+
if (!(0, stagehand_guard_1.isTrustedClickMethod)(first?.method) || !first?.selector)
|
|
10774
11185
|
return true;
|
|
10775
|
-
const failure = await centreObservedClickTarget(frameTarget ?? (0, frame_target_1.mainFrameTarget)(page), first.selector
|
|
11186
|
+
const failure = await centreObservedClickTarget(frameTarget ?? (0, frame_target_1.mainFrameTarget)(page), first.selector);
|
|
10776
11187
|
if (failure)
|
|
10777
11188
|
throw new Error(failure);
|
|
10778
11189
|
return true;
|
|
@@ -10951,11 +11362,15 @@ async function executeStepWithHealing(params) {
|
|
|
10951
11362
|
// burn the step budget probing all of them.
|
|
10952
11363
|
const runnerUp = ranked[1];
|
|
10953
11364
|
if (runnerUp) {
|
|
10954
|
-
|
|
10955
|
-
|
|
10956
|
-
|
|
10957
|
-
|
|
10958
|
-
|
|
11365
|
+
const midTarget = frameTarget ?? (0, frame_target_1.mainFrameTarget)(page);
|
|
11366
|
+
const deepMidFlowSubmit = isMidFlowSubmitShapedStep({
|
|
11367
|
+
step,
|
|
11368
|
+
submitStep,
|
|
11369
|
+
isFinalStep,
|
|
11370
|
+
flowHasSubmitSemantics: flowHasSubmitSemanticsFlag,
|
|
11371
|
+
elementSubmitShaped: !stepDeclaredNonSubmit,
|
|
11372
|
+
});
|
|
11373
|
+
const midPost = await settleAndSnapshotPostClick(midTarget, signalCounter, page, pre, deepMidFlowSubmit);
|
|
10959
11374
|
const topVerdict = (0, phantom_click_1.classifyPhantomClick)({
|
|
10960
11375
|
actResultSuccess: true,
|
|
10961
11376
|
pre,
|
|
@@ -10964,20 +11379,8 @@ async function executeStepWithHealing(params) {
|
|
|
10964
11379
|
// stray selection flip must not mask a top-pick phantom and skip
|
|
10965
11380
|
// the runner-up retry.
|
|
10966
11381
|
isSubmitShapedStep: true,
|
|
10967
|
-
isMidFlowSubmitStep:
|
|
10968
|
-
|
|
10969
|
-
submitStep,
|
|
10970
|
-
isFinalStep,
|
|
10971
|
-
flowHasSubmitSemantics: flowHasSubmitSemanticsFlag,
|
|
10972
|
-
}),
|
|
10973
|
-
destinationPlausible: (0, phantom_click_1.isPlausibleStepDestination)(step, midPost.url, isMidFlowSubmitShapedStep({
|
|
10974
|
-
step,
|
|
10975
|
-
submitStep,
|
|
10976
|
-
isFinalStep,
|
|
10977
|
-
flowHasSubmitSemantics: flowHasSubmitSemanticsFlag,
|
|
10978
|
-
})
|
|
10979
|
-
? pre.url
|
|
10980
|
-
: undefined),
|
|
11382
|
+
isMidFlowSubmitStep: deepMidFlowSubmit,
|
|
11383
|
+
destinationPlausible: (0, phantom_click_1.isPlausibleStepDestination)(step, midPost.url, deepMidFlowSubmit ? pre.url : undefined),
|
|
10981
11384
|
});
|
|
10982
11385
|
if (topVerdict === "phantom") {
|
|
10983
11386
|
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})`);
|
|
@@ -11370,8 +11773,8 @@ async function executeStepWithHealing(params) {
|
|
|
11370
11773
|
// bars, so centre + hit-test the observed control first; an
|
|
11371
11774
|
// unrecoverable occlusion fails the attempt instead of clicking an
|
|
11372
11775
|
// overlay.
|
|
11373
|
-
if (overriddenTarget.method
|
|
11374
|
-
const centreFailure = await centreObservedClickTarget(frameTarget ?? (0, frame_target_1.mainFrameTarget)(page), overriddenTarget.selector
|
|
11776
|
+
if ((0, stagehand_guard_1.isTrustedClickMethod)(overriddenTarget.method)) {
|
|
11777
|
+
const centreFailure = await centreObservedClickTarget(frameTarget ?? (0, frame_target_1.mainFrameTarget)(page), overriddenTarget.selector);
|
|
11375
11778
|
if (centreFailure) {
|
|
11376
11779
|
record.errorMessage = centreFailure;
|
|
11377
11780
|
attempts.push(record);
|
|
@@ -11708,6 +12111,24 @@ async function executeStepWithHealing(params) {
|
|
|
11708
12111
|
}
|
|
11709
12112
|
// A fill/select/type action cannot open a tab, so skip the discovery wait for it.
|
|
11710
12113
|
await adoptOpenedPage(resolvedAction && STATE_CLASS_METHODS.has(resolvedAction.method ?? "") ? 0 : undefined);
|
|
12114
|
+
const isClick = resolvedAction !== null && resolvedAction.method === "click";
|
|
12115
|
+
// Attribute-based submit-shape read of the resolved click target itself —
|
|
12116
|
+
// independent of the flow-authored `submitStep` flag. Read before the
|
|
12117
|
+
// settle: a view swap removes the clicked control, so a post-swap read
|
|
12118
|
+
// would blind the mid-flow exemption on exactly the case it exists for.
|
|
12119
|
+
// See resolvedClickTargetIsSubmitShaped's doc comment.
|
|
12120
|
+
const resolvedElementIsSubmitShaped = isClick && !stepDeclaredNonSubmit && resolvedAction?.selector
|
|
12121
|
+
? await resolvedClickTargetIsSubmitShaped(frameTarget ?? (0, frame_target_1.mainFrameTarget)(page), resolvedAction.selector)
|
|
12122
|
+
: false;
|
|
12123
|
+
// Computed once: gates every credit signal that takes no post-click URL
|
|
12124
|
+
// (network, DOM read-back) and classifyPhantomClick's destinationPlausible.
|
|
12125
|
+
const stepIsMidFlowSubmit = isMidFlowSubmitShapedStep({
|
|
12126
|
+
step,
|
|
12127
|
+
submitStep,
|
|
12128
|
+
isFinalStep,
|
|
12129
|
+
flowHasSubmitSemantics: flowHasSubmitSemanticsFlag,
|
|
12130
|
+
elementSubmitShaped: resolvedElementIsSubmitShaped,
|
|
12131
|
+
});
|
|
11711
12132
|
await page.waitForTimeout(STEP_PAUSE_MS);
|
|
11712
12133
|
// A successful act that reported no actions still ran a click-class action;
|
|
11713
12134
|
// judging it before its round-trip lands would cascade onto the advanced page.
|
|
@@ -11716,10 +12137,7 @@ async function executeStepWithHealing(params) {
|
|
|
11716
12137
|
: !STATE_CLASS_METHODS.has(resolvedAction.method ?? "");
|
|
11717
12138
|
if (mayTriggerRoundTrip) {
|
|
11718
12139
|
const settleTarget = frameTarget ?? (0, frame_target_1.mainFrameTarget)(page);
|
|
11719
|
-
await (
|
|
11720
|
-
const snap = await snapshotPage(settleTarget, signalCounter);
|
|
11721
|
-
return snap.visibleTextSignature;
|
|
11722
|
-
}, pre.visibleTextSignature);
|
|
12140
|
+
await settleWithPageQuiescence(page, settleTarget, signalCounter, pre.networkCount, isSettleWaitStep || resolvedElementIsSubmitShaped, pre.visibleTextSignature, pageAdvancedSince(pre.visibleTextSignature, stepIsMidFlowSubmit));
|
|
11723
12141
|
}
|
|
11724
12142
|
const post = await snapshotPage(frameTarget ?? (0, frame_target_1.mainFrameTarget)(page), signalCounter, page);
|
|
11725
12143
|
record.post = post;
|
|
@@ -11728,18 +12146,9 @@ async function executeStepWithHealing(params) {
|
|
|
11728
12146
|
record.resolvedArguments = (resolvedAction.arguments ?? []).map(String);
|
|
11729
12147
|
}
|
|
11730
12148
|
const networkFired = post.networkCount > pre.networkCount;
|
|
11731
|
-
// Computed once: gates every credit signal that takes no post-click URL
|
|
11732
|
-
// (network, DOM read-back) and classifyPhantomClick's destinationPlausible.
|
|
11733
|
-
const stepIsMidFlowSubmit = isMidFlowSubmitShapedStep({
|
|
11734
|
-
step,
|
|
11735
|
-
submitStep,
|
|
11736
|
-
isFinalStep,
|
|
11737
|
-
flowHasSubmitSemantics: flowHasSubmitSemanticsFlag,
|
|
11738
|
-
});
|
|
11739
12149
|
const destinationPlausible = (0, phantom_click_1.isPlausibleStepDestination)(step, post.url, stepIsMidFlowSubmit ? pre.url : undefined);
|
|
11740
12150
|
const urlChanged = (0, phantom_click_1.hasOriginOrPathChanged)(pre.url, post.url) && destinationPlausible;
|
|
11741
12151
|
const isStateClass = resolvedAction !== null && STATE_CLASS_METHODS.has(resolvedAction.method ?? "");
|
|
11742
|
-
const isClick = resolvedAction !== null && resolvedAction.method === "click";
|
|
11743
12152
|
// State-class actions (fill/check/etc.) never move the network counter or URL,
|
|
11744
12153
|
// so the legacy heuristic false-negatived every form fill. Re-read DOM state
|
|
11745
12154
|
// for those; keep the navigation signal authoritative for clicks/links —
|
|
@@ -11836,12 +12245,6 @@ async function executeStepWithHealing(params) {
|
|
|
11836
12245
|
const clickedElementStillPresent = isClick && resolvedAction?.selector
|
|
11837
12246
|
? await resolvedClickTargetStillPresent(frameTarget ?? (0, frame_target_1.mainFrameTarget)(page), resolvedAction.selector)
|
|
11838
12247
|
: true;
|
|
11839
|
-
// Attribute-based submit-shape read of the resolved click target itself —
|
|
11840
|
-
// independent of the flow-authored `submitStep` flag. See
|
|
11841
|
-
// resolvedClickTargetIsSubmitShaped's doc comment.
|
|
11842
|
-
const resolvedElementIsSubmitShaped = isClick && !stepDeclaredNonSubmit && resolvedAction?.selector
|
|
11843
|
-
? await resolvedClickTargetIsSubmitShaped(frameTarget ?? (0, frame_target_1.mainFrameTarget)(page), resolvedAction.selector)
|
|
11844
|
-
: false;
|
|
11845
12248
|
// Client-side view-swap gate: credit a click that produces substantial
|
|
11846
12249
|
// DOM growth (≥5KB) with zero network when it's NOT a submit/final step
|
|
11847
12250
|
// and NOT an advance-pattern step. Fixes the top-window site "Manual Application"
|
|
@@ -12142,6 +12545,9 @@ async function executeStepWithHealing(params) {
|
|
|
12142
12545
|
captureFn,
|
|
12143
12546
|
step,
|
|
12144
12547
|
isFinalStep,
|
|
12548
|
+
submitStep,
|
|
12549
|
+
flowHasSubmitSemantics: flowHasSubmitSemanticsFlag,
|
|
12550
|
+
elementSubmitShaped: resolvedElementIsSubmitShaped,
|
|
12145
12551
|
page,
|
|
12146
12552
|
target: frameTarget ?? (0, frame_target_1.mainFrameTarget)(page),
|
|
12147
12553
|
signalCounter,
|
|
@@ -12383,10 +12789,13 @@ async function executeStepWithHealing(params) {
|
|
|
12383
12789
|
probeResult.checked === true &&
|
|
12384
12790
|
(probeResult.ariaManaged === true || !ancestorStillInvalid);
|
|
12385
12791
|
await page.waitForTimeout(STEP_PAUSE_MS);
|
|
12386
|
-
await (0,
|
|
12387
|
-
|
|
12388
|
-
|
|
12389
|
-
|
|
12792
|
+
await settleWithPageQuiescence(page, frameTarget ?? (0, frame_target_1.mainFrameTarget)(page), signalCounter, pre.networkCount, isSettleWaitStep || retryResolvedElementIsSubmitShaped, pre.visibleTextSignature, pageAdvancedSince(pre.visibleTextSignature, isMidFlowSubmitShapedStep({
|
|
12793
|
+
step,
|
|
12794
|
+
submitStep,
|
|
12795
|
+
isFinalStep,
|
|
12796
|
+
flowHasSubmitSemantics: flowHasSubmitSemanticsFlag,
|
|
12797
|
+
elementSubmitShaped: retryResolvedElementIsSubmitShaped,
|
|
12798
|
+
})));
|
|
12390
12799
|
const retryPost = await snapshotPage(frameTarget ?? (0, frame_target_1.mainFrameTarget)(page), signalCounter, page);
|
|
12391
12800
|
const retryNetworkFired = retryPost.networkCount > pre.networkCount;
|
|
12392
12801
|
const retryMidFlowSubmit = isMidFlowSubmitShapedStep({
|
|
@@ -12738,6 +13147,9 @@ async function executeStepWithHealing(params) {
|
|
|
12738
13147
|
captureFn,
|
|
12739
13148
|
step,
|
|
12740
13149
|
isFinalStep,
|
|
13150
|
+
submitStep,
|
|
13151
|
+
flowHasSubmitSemantics: flowHasSubmitSemanticsFlag,
|
|
13152
|
+
elementSubmitShaped: resolvedElementIsSubmitShaped,
|
|
12741
13153
|
page,
|
|
12742
13154
|
target: frameTarget ?? (0, frame_target_1.mainFrameTarget)(page),
|
|
12743
13155
|
signalCounter,
|
|
@@ -13093,7 +13505,7 @@ async function executeStepWithHealing(params) {
|
|
|
13093
13505
|
if (dumpPath !== null) {
|
|
13094
13506
|
logger.error(`${formatStepPrefix(stepIndex, totalSteps)} failed after ${MAX_STEP_ATTEMPTS} attempts; diagnostic bundle: ${dumpPath}`);
|
|
13095
13507
|
}
|
|
13096
|
-
throw new errors_2.StepVerificationError(`${formatStepPrefix(stepIndex, totalSteps)} (${(0, pii_redaction_1.
|
|
13508
|
+
throw new errors_2.StepVerificationError(`${formatStepPrefix(stepIndex, totalSteps)} (${(0, pii_redaction_1.redactPii)(step).slice(0, 60)}) failed verification after ${MAX_STEP_ATTEMPTS} attempts${dumpPath ? `; see ${dumpPath}` : ""}`, phantomClickAfterAttempt1 ? "phantom-click-exhausted" : "cascade-exhausted");
|
|
13097
13509
|
}
|
|
13098
13510
|
/**
|
|
13099
13511
|
* Navigates `page` directly to `url` and waits for the SPA to hydrate,
|