@enricai/barnacle 1.12.80 → 1.12.81
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/scraper/click-settle.d.ts +35 -1
- package/dist/scraper/click-settle.d.ts.map +1 -1
- package/dist/scraper/click-settle.js +71 -1
- package/dist/scraper/click-settle.js.map +1 -1
- package/dist/scraper/flow-runner.d.ts +32 -6
- package/dist/scraper/flow-runner.d.ts.map +1 -1
- package/dist/scraper/flow-runner.js +496 -142
- package/dist/scraper/flow-runner.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/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;
|
|
@@ -88,6 +89,7 @@ exports.parseFillStep = parseFillStep;
|
|
|
88
89
|
exports.parseFillValueIntent = parseFillValueIntent;
|
|
89
90
|
exports.parseSegmentedDateValue = parseSegmentedDateValue;
|
|
90
91
|
exports.resolveSegmentedDateGroup = resolveSegmentedDateGroup;
|
|
92
|
+
exports.parseSegmentedPartStep = parseSegmentedPartStep;
|
|
91
93
|
exports.trySegmentedDateSpinbuttonPrimitive = trySegmentedDateSpinbuttonPrimitive;
|
|
92
94
|
exports.parseRadioStep = parseRadioStep;
|
|
93
95
|
exports.parseWidgetOptionClickStep = parseWidgetOptionClickStep;
|
|
@@ -733,6 +735,18 @@ const PROMPT_CURRENT_TEXT_EXPR = `((w, valueSel, emptyRx) => {
|
|
|
733
735
|
if (raw.trim() && !emptyRx.test(raw.trim())) return norm(raw);
|
|
734
736
|
return "";
|
|
735
737
|
})`;
|
|
738
|
+
/**
|
|
739
|
+
* Browser-side predicate: does a widget's committed text name the requested
|
|
740
|
+
* option as a whole phrase? A bare substring test credits "Astrophysics" for
|
|
741
|
+
* "Physics"; comparing on word boundaries (punctuation folded to spaces) keeps
|
|
742
|
+
* chip lists and "N items selected, X" announcements matching while a longer,
|
|
743
|
+
* different option no longer does.
|
|
744
|
+
*/
|
|
745
|
+
const PROMPT_NAMES_OPTION_EXPR = `((committed, wanted) => {
|
|
746
|
+
const words = (s) => " " + (s || "").toLowerCase().replace(/[^a-z0-9]+/g, " ").trim() + " ";
|
|
747
|
+
const want = words(wanted);
|
|
748
|
+
return want.trim() === "" || words(committed).includes(want);
|
|
749
|
+
})`;
|
|
736
750
|
/**
|
|
737
751
|
* Browser-side expression reading what a prompt-selector widget ANNOUNCES it
|
|
738
752
|
* committed, lowercased and whitespace-collapsed, or `""` when nothing is
|
|
@@ -764,6 +778,8 @@ const PROMPT_ANNOUNCED_VALUE_EXPR = `((w) => {
|
|
|
764
778
|
let anode = w;
|
|
765
779
|
for (let d = 0; d < ${browser_click_expr_1.MAX_SELECTION_ANCESTOR_DEPTH} && anode; d++) {
|
|
766
780
|
const list = anode.querySelector ? anode.querySelector('[data-automation-id="selectedItemList"]') : null;
|
|
781
|
+
const entries = list ? Array.from(list.querySelectorAll("*")).filter((e) => e.children.length === 0).map((e) => norm(e.textContent)).filter(Boolean) : [];
|
|
782
|
+
if (entries.length > 0) return entries.join(" , ");
|
|
767
783
|
if (list && norm(list.textContent)) return norm(list.textContent);
|
|
768
784
|
anode = anode.parentElement;
|
|
769
785
|
}
|
|
@@ -933,26 +949,15 @@ const N16_TRUSTED_CLICK_TIMEOUT_MS = 3_000;
|
|
|
933
949
|
/**
|
|
934
950
|
* Shared pre-click centre + hit-test for a Stagehand-resolved selector, so
|
|
935
951
|
* observe-act and the natural-language act attempts share one routine.
|
|
936
|
-
* Returns a failure message when the control
|
|
952
|
+
* Returns a failure message when the control cannot be centred and hit-tested
|
|
953
|
+
* (covered, unresolved, or the page cannot answer the probe), null otherwise.
|
|
937
954
|
*/
|
|
938
|
-
async function centreObservedClickTarget(target, selector
|
|
955
|
+
async function centreObservedClickTarget(target, selector) {
|
|
939
956
|
try {
|
|
940
957
|
await (0, trusted_click_1.centreSelectorForClick)(target, selector);
|
|
941
958
|
return null;
|
|
942
959
|
}
|
|
943
960
|
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
961
|
return `observed click target not clickable: ${(0, errors_1.toErrorMessage)(err)}`;
|
|
957
962
|
}
|
|
958
963
|
}
|
|
@@ -1151,6 +1156,14 @@ function fieldValuesAtFailureExpr() {
|
|
|
1151
1156
|
return out;
|
|
1152
1157
|
})()`;
|
|
1153
1158
|
}
|
|
1159
|
+
/**
|
|
1160
|
+
* Settles after a click using the target's own text signature and in-page
|
|
1161
|
+
* quiescence reader, so every post-click site decides from the same settle
|
|
1162
|
+
* signal instead of hand-building the readers.
|
|
1163
|
+
*/
|
|
1164
|
+
async function settleWithPageQuiescence(page, target, signalCounter, preNetworkCount, submitShaped, preVisibleTextSignature) {
|
|
1165
|
+
await (0, click_settle_1.settleAfterClick)(page, signalCounter, preNetworkCount, submitShaped, async () => (await snapshotPage(target, signalCounter)).visibleTextSignature, preVisibleTextSignature, async () => target.evaluate(click_settle_1.QUIESCENCE_PROBE_EXPR));
|
|
1166
|
+
}
|
|
1154
1167
|
/**
|
|
1155
1168
|
* Captures the pre/post signal triple the submit-verify cascade diffs.
|
|
1156
1169
|
* Accepts the optional `page` so a resolved child `FrameTarget` whose
|
|
@@ -2905,7 +2918,7 @@ async function judgeStepOutcome(params) {
|
|
|
2905
2918
|
await (0, click_settle_1.settleAfterClick)(params.page, params.signalCounter, params.preNetworkCount, isStepSettleWaitShaped({ step, submitStep: false, isFinalStep, flowHasSubmitSemantics: false }), async () => {
|
|
2906
2919
|
const text = await probeRawVisibleText(target);
|
|
2907
2920
|
return `${text.length}:${text.slice(0, 200)}`;
|
|
2908
|
-
}, params.preVisibleTextSignature);
|
|
2921
|
+
}, params.preVisibleTextSignature, async () => target.evaluate(click_settle_1.QUIESCENCE_PROBE_EXPR));
|
|
2909
2922
|
const colon = params.preVisibleTextSignature.indexOf(":");
|
|
2910
2923
|
const beforeText = (colon === -1 ? params.preVisibleTextSignature : params.preVisibleTextSignature.slice(colon + 1))
|
|
2911
2924
|
.replace(/\s+/g, " ")
|
|
@@ -3557,8 +3570,8 @@ async function verifyFillReadback(target, selector, expectedValue) {
|
|
|
3557
3570
|
if (!el) return null;
|
|
3558
3571
|
const tag = el.tagName ? el.tagName.toLowerCase() : "";
|
|
3559
3572
|
let actual = "";
|
|
3560
|
-
|
|
3561
|
-
if (el
|
|
3573
|
+
${SEGMENT_OWNER_JS}
|
|
3574
|
+
if (isDateSegment(el)) {
|
|
3562
3575
|
return { outcome: "segment", postValue: el.value || el.getAttribute("aria-valuenow") || "", tag };
|
|
3563
3576
|
}
|
|
3564
3577
|
if (el.tagName === "INPUT" || el.tagName === "TEXTAREA") {
|
|
@@ -3624,8 +3637,8 @@ async function verifyPromptSelectorCommitted(target, selector, requestedOption)
|
|
|
3624
3637
|
const emptyRx = new RegExp(${JSON.stringify(PROMPT_EMPTY_VALUE_RX_SRC)}, ${JSON.stringify(PROMPT_EMPTY_VALUE_RX_FLAGS)});
|
|
3625
3638
|
const currentText = ${PROMPT_CURRENT_TEXT_EXPR};
|
|
3626
3639
|
const announcedValue = ${PROMPT_ANNOUNCED_VALUE_EXPR};
|
|
3627
|
-
const
|
|
3628
|
-
const want =
|
|
3640
|
+
const namesOption = ${PROMPT_NAMES_OPTION_EXPR};
|
|
3641
|
+
const want = ${JSON.stringify(requestedOption ?? "")};
|
|
3629
3642
|
const xpath = ${JSON.stringify(xpath)};
|
|
3630
3643
|
const result = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
|
|
3631
3644
|
const el = result.singleNodeValue;
|
|
@@ -3633,7 +3646,7 @@ async function verifyPromptSelectorCommitted(target, selector, requestedOption)
|
|
|
3633
3646
|
const widget = el.matches && el.matches(triggerSel) ? el : el.closest(triggerSel);
|
|
3634
3647
|
if (!widget) return { isPromptWidget: false, committed: false };
|
|
3635
3648
|
const value = announcedValue(widget) || currentText(widget, valueSel, emptyRx);
|
|
3636
|
-
return { isPromptWidget: true, committed: value !== "" && (!want ||
|
|
3649
|
+
return { isPromptWidget: true, committed: value !== "" && (!want || namesOption(value, want)) };
|
|
3637
3650
|
})()`;
|
|
3638
3651
|
try {
|
|
3639
3652
|
const raw = await target.evaluate(expr);
|
|
@@ -5269,30 +5282,108 @@ function segmentedDateStepTokens(text) {
|
|
|
5269
5282
|
return named ? [token] : [];
|
|
5270
5283
|
});
|
|
5271
5284
|
}
|
|
5285
|
+
const SEGMENTED_DATE_STEP_NOISE = new Set([
|
|
5286
|
+
"barnaclevalue",
|
|
5287
|
+
"click",
|
|
5288
|
+
"type",
|
|
5289
|
+
"enter",
|
|
5290
|
+
"fill",
|
|
5291
|
+
"set",
|
|
5292
|
+
"select",
|
|
5293
|
+
"input",
|
|
5294
|
+
"write",
|
|
5295
|
+
"put",
|
|
5296
|
+
"choose",
|
|
5297
|
+
"use",
|
|
5298
|
+
"press",
|
|
5299
|
+
"focus",
|
|
5300
|
+
"spinbutton",
|
|
5301
|
+
"part",
|
|
5302
|
+
"segment",
|
|
5303
|
+
"digits",
|
|
5304
|
+
"box",
|
|
5305
|
+
"first",
|
|
5306
|
+
"second",
|
|
5307
|
+
"into",
|
|
5308
|
+
"as",
|
|
5309
|
+
"by",
|
|
5310
|
+
"from",
|
|
5311
|
+
"to",
|
|
5312
|
+
"is",
|
|
5313
|
+
"be",
|
|
5314
|
+
"it",
|
|
5315
|
+
"its",
|
|
5316
|
+
"this",
|
|
5317
|
+
"that",
|
|
5318
|
+
"my",
|
|
5319
|
+
"your",
|
|
5320
|
+
...SEGMENTED_DATE_STOPWORDS,
|
|
5321
|
+
...SEGMENTED_DATE_FIELD_WORDS,
|
|
5322
|
+
...SEGMENTED_DATE_PART_LABELS,
|
|
5323
|
+
...SEGMENTED_DATE_FIELD_NOUNS,
|
|
5324
|
+
]);
|
|
5272
5325
|
/**
|
|
5273
5326
|
* Pick which spinbutton group a segmented-date step targets. Groups whose
|
|
5274
5327
|
* segment widths (digit count of `aria-valuemax`) don't sum to the value's
|
|
5275
5328
|
* digit count are dropped. A group is only eligible when a field the step names
|
|
5276
5329
|
* (quoted, e.g. 'From', or unquoted in field position, e.g. "the To date")
|
|
5277
5330
|
* matches its OWN label as whole words — a group with no `label` is judged by
|
|
5278
|
-
* its whole `context
|
|
5331
|
+
* its whole `context` — and every other quoted phrase is found in the group's
|
|
5332
|
+
* label or section. The section the step names ("Work Experience 1") is
|
|
5279
5333
|
* 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
|
-
*
|
|
5334
|
+
* section words better than any eligible group does, or the group's own
|
|
5335
|
+
* section shares none of the step's remaining words (or numbering), the named
|
|
5336
|
+
* section renders no such field and the step resolves to nothing rather than to
|
|
5337
|
+
* another section's. Returns the index into `groups`, or `null` when nothing
|
|
5338
|
+
* matches or the best match is tied — writing a date into a group that merely
|
|
5339
|
+
* fits the width would corrupt another field. Groups that hold the same
|
|
5340
|
+
* segments under different names never tie.
|
|
5285
5341
|
*/
|
|
5286
5342
|
function resolveSegmentedDateGroup(instruction, groups) {
|
|
5287
5343
|
const value = parseSegmentedDateValue(instruction);
|
|
5288
5344
|
if (!value)
|
|
5289
5345
|
return null;
|
|
5346
|
+
const compatible = (group) => group.segmentMaxes.map((max) => String(max).length).reduce((a, b) => a + b, 0) ===
|
|
5347
|
+
value.digits.length;
|
|
5348
|
+
return pickSegmentedDateGroup(instruction, groups, value, compatible);
|
|
5349
|
+
}
|
|
5350
|
+
const SEGMENTED_DATE_PART_BY_LABEL = {
|
|
5351
|
+
month: "month",
|
|
5352
|
+
mm: "month",
|
|
5353
|
+
day: "day",
|
|
5354
|
+
dd: "day",
|
|
5355
|
+
year: "year",
|
|
5356
|
+
yyyy: "year",
|
|
5357
|
+
yy: "year",
|
|
5358
|
+
};
|
|
5359
|
+
/**
|
|
5360
|
+
* Parse a per-segment step such as "type '01' into Month". Requires a quoted
|
|
5361
|
+
* 1-4 digit value AND exactly one part label (Month/MM, Day/DD, Year/YYYY), so
|
|
5362
|
+
* a bare short numeric (ZIP fragment, age) is never claimed.
|
|
5363
|
+
*/
|
|
5364
|
+
function parseSegmentedPartStep(instruction) {
|
|
5365
|
+
const raw = parseFillValueIntent(instruction)?.value;
|
|
5366
|
+
if (!raw || !/^\d{1,4}$/.test(raw))
|
|
5367
|
+
return null;
|
|
5368
|
+
const parts = new Set(segmentedDateTokens(instruction.replace(raw, " ")).flatMap((t) => {
|
|
5369
|
+
const part = SEGMENTED_DATE_PART_BY_LABEL[t];
|
|
5370
|
+
return part ? [part] : [];
|
|
5371
|
+
}));
|
|
5372
|
+
const [part] = [...parts];
|
|
5373
|
+
return parts.size === 1 && part ? { digits: raw, valueSpan: raw, part } : null;
|
|
5374
|
+
}
|
|
5375
|
+
/** Index of the single segment in `group` whose own label names `part`, or null when none/ambiguous. */
|
|
5376
|
+
function segmentIndexForPart(group, part) {
|
|
5377
|
+
const hits = (group.segmentLabels ?? []).flatMap((label, si) => segmentedDateTokens(label).some((t) => SEGMENTED_DATE_PART_BY_LABEL[t] === part) ? [si] : []);
|
|
5378
|
+
return hits.length === 1 ? (hits[0] ?? null) : null;
|
|
5379
|
+
}
|
|
5380
|
+
function pickSegmentedDateGroup(instruction, groups, value, compatible) {
|
|
5290
5381
|
const valueTokens = segmentedDateTokens(value.valueSpan).join(" ");
|
|
5291
5382
|
const quoted = (0, quoted_spans_1.findQuotedSpans)(instruction)
|
|
5292
5383
|
.map((s) => segmentedDateTokens(s.text.trim()))
|
|
5293
5384
|
.filter((t) => t.length > 0 &&
|
|
5294
5385
|
t.join(" ") !== valueTokens &&
|
|
5295
|
-
!
|
|
5386
|
+
!t.every((token) => SEGMENTED_DATE_PART_LABELS.has(token)));
|
|
5296
5387
|
const stepTokens = segmentedDateStepTokens(instruction.replace(value.valueSpan, " barnaclevalue "));
|
|
5297
5388
|
const targets = quoted.length > 0 ? quoted : [stepTokens];
|
|
5298
5389
|
const contains = (hay, needle) => {
|
|
@@ -5302,8 +5393,6 @@ function resolveSegmentedDateGroup(instruction, groups) {
|
|
|
5302
5393
|
}
|
|
5303
5394
|
return false;
|
|
5304
5395
|
};
|
|
5305
|
-
const compatible = (group) => group.segmentMaxes.map((max) => String(max).length).reduce((a, b) => a + b, 0) ===
|
|
5306
|
-
value.digits.length;
|
|
5307
5396
|
const stepSet = new Set(stepTokens);
|
|
5308
5397
|
const sectionScore = (group) => {
|
|
5309
5398
|
const own = new Set(segmentedDateTokens(group.label ?? ""));
|
|
@@ -5313,10 +5402,33 @@ function resolveSegmentedDateGroup(instruction, groups) {
|
|
|
5313
5402
|
!SEGMENTED_DATE_STOPWORDS.has(t) &&
|
|
5314
5403
|
!SEGMENTED_DATE_FIELD_WORDS.has(t)).length;
|
|
5315
5404
|
};
|
|
5405
|
+
const quotedTokens = new Set(quoted.flat());
|
|
5406
|
+
const leftover = stepTokens.filter((t) => !SEGMENTED_DATE_STEP_NOISE.has(t) && !quotedTokens.has(t));
|
|
5407
|
+
const isNumber = (t) => /^\d+$/.test(t);
|
|
5408
|
+
// A group under another heading is not the field the step names, however
|
|
5409
|
+
// well its label matches: the step's own words must find the section.
|
|
5410
|
+
// A section that only repeats the group's own label is no heading at all
|
|
5411
|
+
// (Workday enumerates "From*" as both), so it cannot name another section.
|
|
5412
|
+
const namesOtherSection = (group) => {
|
|
5413
|
+
const section = segmentedDateTokens(group.section ?? "");
|
|
5414
|
+
const label = segmentedDateTokens(group.label ?? "");
|
|
5415
|
+
if (section.length === 0 || contains(label, section))
|
|
5416
|
+
return false;
|
|
5417
|
+
const known = new Set([...section, ...label]);
|
|
5418
|
+
const words = leftover.filter((t) => !isNumber(t));
|
|
5419
|
+
const numbers = leftover.filter(isNumber);
|
|
5420
|
+
const sectionNumbers = section.filter(isNumber);
|
|
5421
|
+
if (numbers.length > 0 && sectionNumbers.length > 0) {
|
|
5422
|
+
if (!numbers.some((n) => sectionNumbers.includes(n)))
|
|
5423
|
+
return true;
|
|
5424
|
+
}
|
|
5425
|
+
if (words.some((t) => section.includes(t)))
|
|
5426
|
+
return false;
|
|
5427
|
+
return words.some((t) => !known.has(t));
|
|
5428
|
+
};
|
|
5316
5429
|
const bestSection = Math.max(0, ...groups.filter(compatible).map((group) => sectionScore(group)));
|
|
5317
|
-
let best = -1;
|
|
5318
5430
|
let bestScore = -1;
|
|
5319
|
-
let
|
|
5431
|
+
let leaders = [];
|
|
5320
5432
|
groups.forEach((group, index) => {
|
|
5321
5433
|
if (!compatible(group))
|
|
5322
5434
|
return;
|
|
@@ -5326,24 +5438,43 @@ function resolveSegmentedDateGroup(instruction, groups) {
|
|
|
5326
5438
|
const namesOwnField = group.label === undefined &&
|
|
5327
5439
|
quoted.length === 0 &&
|
|
5328
5440
|
own.some((t) => SEGMENTED_DATE_FIELD_WORDS.has(t) && stepSet.has(t));
|
|
5329
|
-
|
|
5330
|
-
|
|
5441
|
+
const namesOwnLabel = (t) => contains(t, own) || (quoted.length > 0 && contains(own, t));
|
|
5442
|
+
if (!namesOwnField && !targets.some(namesOwnLabel))
|
|
5443
|
+
return;
|
|
5444
|
+
const named = segmentedDateTokens(`${group.section ?? ""} ${group.context}`);
|
|
5445
|
+
if (quoted.some((t) => !namesOwnLabel(t) && !contains(named, t)))
|
|
5446
|
+
return;
|
|
5447
|
+
if (namesOtherSection(group))
|
|
5331
5448
|
return;
|
|
5332
|
-
}
|
|
5333
5449
|
const score = sectionScore(group);
|
|
5334
5450
|
if (score < bestSection)
|
|
5335
5451
|
return;
|
|
5336
5452
|
if (score > bestScore) {
|
|
5337
|
-
best = index;
|
|
5338
5453
|
bestScore = score;
|
|
5339
|
-
|
|
5454
|
+
leaders = [index];
|
|
5340
5455
|
}
|
|
5341
5456
|
else if (score === bestScore) {
|
|
5342
|
-
|
|
5457
|
+
const alias = (a, b) => (groups[a]?.sameAs ?? a) === (groups[b]?.sameAs ?? b) ||
|
|
5458
|
+
groups[a]?.sameAs === b ||
|
|
5459
|
+
groups[b]?.sameAs === a;
|
|
5460
|
+
const twin = leaders.findIndex((leader) => alias(leader, index));
|
|
5461
|
+
if (twin < 0)
|
|
5462
|
+
leaders.push(index);
|
|
5463
|
+
else
|
|
5464
|
+
leaders[twin] = Math.min(leaders[twin] ?? index, index);
|
|
5343
5465
|
}
|
|
5344
5466
|
});
|
|
5345
|
-
if (
|
|
5346
|
-
return
|
|
5467
|
+
if (leaders.length === 1)
|
|
5468
|
+
return leaders[0] ?? null;
|
|
5469
|
+
if (leaders.length > 1) {
|
|
5470
|
+
// A nested group is one wrapper's slice of a larger date widget (e.g. the
|
|
5471
|
+
// 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 — Workday renders
|
|
5473
|
+
// "Work Experience 1 From" and "Education 1 From" with no heading the
|
|
5474
|
+
// enumerator can see. The standalone group is the field the step names.
|
|
5475
|
+
const standalone = leaders.filter((i) => !groups[i]?.nested);
|
|
5476
|
+
return standalone.length === 1 ? (standalone[0] ?? null) : null;
|
|
5477
|
+
}
|
|
5347
5478
|
// A step that names no field at all can only mean the one date widget on the
|
|
5348
5479
|
// page; a named field that matched no label must never fall back to it.
|
|
5349
5480
|
if (quoted.length > 0 || stepTokens.some((t) => SEGMENTED_DATE_FIELD_WORDS.has(t)))
|
|
@@ -5351,12 +5482,12 @@ function resolveSegmentedDateGroup(instruction, groups) {
|
|
|
5351
5482
|
const only = groups.flatMap((g, i) => (compatible(g) && !g.nested ? [i] : []));
|
|
5352
5483
|
return only.length === 1 ? (only[0] ?? null) : null;
|
|
5353
5484
|
}
|
|
5354
|
-
|
|
5355
|
-
|
|
5356
|
-
|
|
5357
|
-
|
|
5358
|
-
|
|
5359
|
-
|
|
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 = `
|
|
5360
5491
|
const hiddenAttr = (node) => node.hasAttribute("hidden") || node.getAttribute("aria-hidden") === "true";
|
|
5361
5492
|
const styleOf = (node) => {
|
|
5362
5493
|
try { return typeof getComputedStyle === "function" ? getComputedStyle(node) : window.getComputedStyle(node); } catch { return null; }
|
|
@@ -5370,18 +5501,53 @@ exports.SEGMENTED_DATE_ENUMERATE_EXPR = `(() => {
|
|
|
5370
5501
|
return false;
|
|
5371
5502
|
};
|
|
5372
5503
|
const spins = Array.from(document.querySelectorAll("input[role='spinbutton']")).filter((el) => !isHidden(el));
|
|
5373
|
-
if (spins.length === 0) return { groups: [] };
|
|
5374
5504
|
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
5505
|
const idText = (ids) => (ids || "").split(/\\s+/).map((id) => {
|
|
5382
5506
|
const ref = id ? document.getElementById(id) : null;
|
|
5383
5507
|
return ref ? norm(ref.textContent) : "";
|
|
5384
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
|
+
const SEGMENTED_DATE_MARKER_ATTR = "data-barnacle-segdate";
|
|
5544
|
+
/** Exported so tests can run the real DOM enumeration against markup. */
|
|
5545
|
+
exports.SEGMENTED_DATE_ENUMERATE_EXPR = `(() => {
|
|
5546
|
+
const attr = ${JSON.stringify(SEGMENTED_DATE_MARKER_ATTR)};
|
|
5547
|
+
const stale = document.querySelectorAll("[" + attr + "]");
|
|
5548
|
+
stale.forEach((el) => el.removeAttribute(attr));
|
|
5549
|
+
${SEGMENT_OWNER_JS}
|
|
5550
|
+
if (spins.length === 0) return { groups: [] };
|
|
5385
5551
|
const headingSibling = (node) => {
|
|
5386
5552
|
let prevSib = node.previousElementSibling;
|
|
5387
5553
|
for (let hop = 0; hop < 3 && prevSib; hop++, prevSib = prevSib.previousElementSibling) {
|
|
@@ -5389,11 +5555,6 @@ exports.SEGMENTED_DATE_ENUMERATE_EXPR = `(() => {
|
|
|
5389
5555
|
}
|
|
5390
5556
|
return "";
|
|
5391
5557
|
};
|
|
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
5558
|
const sectionOf = (box, includeSelf) => {
|
|
5398
5559
|
const parts = [includeSelf ? namedBy(box) : "", headingSibling(box)];
|
|
5399
5560
|
let node = box.parentElement;
|
|
@@ -5408,11 +5569,12 @@ exports.SEGMENTED_DATE_ENUMERATE_EXPR = `(() => {
|
|
|
5408
5569
|
const prev = box.previousElementSibling;
|
|
5409
5570
|
return prev && prev.matches("label,legend") && !prev.querySelector("input,[role='spinbutton']") ? norm(prev.textContent) : "";
|
|
5410
5571
|
};
|
|
5411
|
-
const
|
|
5412
|
-
const
|
|
5413
|
-
const
|
|
5414
|
-
|
|
5415
|
-
|
|
5572
|
+
const byContainer = new Map();
|
|
5573
|
+
for (const el of spins) {
|
|
5574
|
+
const box = ownerOf(el);
|
|
5575
|
+
if (!byContainer.has(box)) byContainer.set(box, []);
|
|
5576
|
+
byContainer.get(box).push(el);
|
|
5577
|
+
}
|
|
5416
5578
|
const labelText = (seg) => {
|
|
5417
5579
|
const parts = [idText(seg.getAttribute("aria-labelledby")), norm(seg.getAttribute("aria-label")), norm(seg.getAttribute("placeholder"))];
|
|
5418
5580
|
if (seg.id) {
|
|
@@ -5441,21 +5603,65 @@ exports.SEGMENTED_DATE_ENUMERATE_EXPR = `(() => {
|
|
|
5441
5603
|
const addGroup = (segs, box, nested) => {
|
|
5442
5604
|
const gi = groups.length;
|
|
5443
5605
|
segs.forEach((seg, si) => mark(seg, gi + ":" + si));
|
|
5444
|
-
|
|
5445
|
-
|
|
5446
|
-
|
|
5447
|
-
|
|
5606
|
+
let inner = null;
|
|
5607
|
+
for (let node = segs[0].parentElement; node && node !== box && box.contains(node) && !inner; node = node.parentElement) {
|
|
5608
|
+
if (segs.every((seg) => node.contains(seg)) && hasCaption(node)) inner = node;
|
|
5609
|
+
}
|
|
5610
|
+
const labelBox = inner || box;
|
|
5611
|
+
const own = namedBy(labelBox) || ownCaption(labelBox) || segs.map((seg) => idText(seg.getAttribute("aria-labelledby"))).filter(Boolean).join(" ");
|
|
5612
|
+
const section = sectionOf(labelBox, Boolean(inner));
|
|
5448
5613
|
groups.push({
|
|
5449
5614
|
segmentMaxes: segs.map((seg, si) => maxOf(seg, si, segs.length)),
|
|
5450
5615
|
context: [section, own].filter(Boolean).join(" "),
|
|
5451
5616
|
label: own,
|
|
5452
5617
|
section,
|
|
5453
5618
|
nested,
|
|
5619
|
+
segmentLabels: segs.map((seg) => labelText(seg)),
|
|
5454
5620
|
});
|
|
5455
5621
|
};
|
|
5622
|
+
const PART_WORD = /^(mm|dd|yyyy|yy|month|day|year)$/i;
|
|
5623
|
+
const keyOf = (text) => norm(text).split(" ").filter((word) => {
|
|
5624
|
+
const parts = word.split(/[^A-Za-z0-9]+/).filter(Boolean);
|
|
5625
|
+
return parts.length > 0 && !parts.every((part) => PART_WORD.test(part));
|
|
5626
|
+
}).join(" ");
|
|
5627
|
+
const ownKey = (seg) => {
|
|
5628
|
+
const parts = [idText(seg.getAttribute("aria-labelledby")), norm(seg.getAttribute("aria-label"))];
|
|
5629
|
+
if (seg.id) {
|
|
5630
|
+
document.querySelectorAll("label").forEach((l) => { if (l.getAttribute("for") === seg.id) parts.push(norm(l.textContent)); });
|
|
5631
|
+
}
|
|
5632
|
+
const wrap = seg.closest("label");
|
|
5633
|
+
if (wrap) parts.push(norm(wrap.textContent));
|
|
5634
|
+
return keyOf(parts.filter(Boolean).join(" "));
|
|
5635
|
+
};
|
|
5636
|
+
// A container's legend names the whole section; the field a segment belongs
|
|
5637
|
+
// to is the caption naming it, or the caption nearest before it with no other
|
|
5638
|
+
// control in between.
|
|
5639
|
+
const fieldKeys = (segs, box) => {
|
|
5640
|
+
const own = new Set(segs);
|
|
5641
|
+
const boxLegend = box.tagName === "FIELDSET" ? box.querySelector(":scope > legend") : null;
|
|
5642
|
+
const keys = new Map();
|
|
5643
|
+
let current = "";
|
|
5644
|
+
box.querySelectorAll("label,legend,input,select,textarea,button").forEach((el) => {
|
|
5645
|
+
if (el === boxLegend) return;
|
|
5646
|
+
if (el.matches("label,legend")) {
|
|
5647
|
+
const target = el.getAttribute("for");
|
|
5648
|
+
if (isHidden(el) || el.querySelector("input,select,textarea,[role='spinbutton']") || (target && box.querySelector("[id='" + target + "']"))) return;
|
|
5649
|
+
current = keyOf(el.textContent);
|
|
5650
|
+
return;
|
|
5651
|
+
}
|
|
5652
|
+
if (!own.has(el)) {
|
|
5653
|
+
if (!isHidden(el)) current = "";
|
|
5654
|
+
return;
|
|
5655
|
+
}
|
|
5656
|
+
keys.set(el, ownKey(el) || current);
|
|
5657
|
+
});
|
|
5658
|
+
return segs.map((seg) => keys.get(seg) || "");
|
|
5659
|
+
};
|
|
5660
|
+
const captionRuns = [];
|
|
5456
5661
|
const nestedCandidates = [];
|
|
5457
5662
|
byContainer.forEach((segs, box) => {
|
|
5458
5663
|
addGroup(segs, box, false);
|
|
5664
|
+
const outerIndex = groups.length - 1;
|
|
5459
5665
|
const byParent = new Map();
|
|
5460
5666
|
for (const seg of segs) {
|
|
5461
5667
|
if (!byParent.has(seg.parentElement)) byParent.set(seg.parentElement, []);
|
|
@@ -5463,9 +5669,36 @@ exports.SEGMENTED_DATE_ENUMERATE_EXPR = `(() => {
|
|
|
5463
5669
|
}
|
|
5464
5670
|
// A shared fieldset can hold several independent dates (From and To year
|
|
5465
5671
|
// spinbuttons side by side); each wrapper is then its own candidate group.
|
|
5466
|
-
if (byParent.size > 1)
|
|
5672
|
+
if (byParent.size > 1) {
|
|
5673
|
+
byParent.forEach((inner, parent) => nestedCandidates.push([inner, parent]));
|
|
5674
|
+
return;
|
|
5675
|
+
}
|
|
5676
|
+
const keys = fieldKeys(segs, box);
|
|
5677
|
+
const runs = [];
|
|
5678
|
+
segs.forEach((seg, si) => {
|
|
5679
|
+
const last = runs[runs.length - 1];
|
|
5680
|
+
if (last && last.key === keys[si]) last.segs.push(seg);
|
|
5681
|
+
else runs.push({ key: keys[si], segs: [seg] });
|
|
5682
|
+
});
|
|
5683
|
+
runs.filter((run) => run.key).forEach((run) => captionRuns.push({ run, box, outerIndex, whole: run.segs.length === segs.length }));
|
|
5467
5684
|
});
|
|
5468
5685
|
nestedCandidates.forEach(([inner, parent]) => addGroup(inner, parent, true));
|
|
5686
|
+
captionRuns.forEach(({ run, box, outerIndex, whole }) => {
|
|
5687
|
+
const outer = groups[outerIndex];
|
|
5688
|
+
const same = (a, b) => a.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim() === b.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
|
|
5689
|
+
if (whole && same(outer.label || "", run.key)) return;
|
|
5690
|
+
const gi = groups.length;
|
|
5691
|
+
run.segs.forEach((seg, si) => mark(seg, gi + ":" + si));
|
|
5692
|
+
const section = sectionOf(box, true);
|
|
5693
|
+
groups.push({
|
|
5694
|
+
segmentMaxes: run.segs.map((seg, si) => maxOf(seg, si, run.segs.length)),
|
|
5695
|
+
context: [section, run.key].filter(Boolean).join(" "),
|
|
5696
|
+
label: run.key,
|
|
5697
|
+
section,
|
|
5698
|
+
nested: true,
|
|
5699
|
+
sameAs: whole ? outerIndex : undefined,
|
|
5700
|
+
});
|
|
5701
|
+
});
|
|
5469
5702
|
return { groups };
|
|
5470
5703
|
})()`;
|
|
5471
5704
|
const SEGMENTED_DATE_CLEAR_EXPR = `(() => {
|
|
@@ -5484,15 +5717,29 @@ const SEGMENTED_DATE_CLEAR_EXPR = `(() => {
|
|
|
5484
5717
|
*/
|
|
5485
5718
|
async function trySegmentedDateSpinbuttonPrimitive(params) {
|
|
5486
5719
|
const { page, target, instruction, logger } = params;
|
|
5487
|
-
const
|
|
5488
|
-
|
|
5720
|
+
const whole = parseSegmentedDateValue(instruction);
|
|
5721
|
+
const part = parseSegmentedPartStep(instruction);
|
|
5722
|
+
if (!whole && !part)
|
|
5489
5723
|
return null;
|
|
5490
|
-
const sendCDP = (
|
|
5724
|
+
const sendCDP = frameCdpSender(page, target);
|
|
5491
5725
|
try {
|
|
5492
5726
|
const enumerated = (await target.evaluate(exports.SEGMENTED_DATE_ENUMERATE_EXPR));
|
|
5493
5727
|
if (!enumerated?.groups?.length)
|
|
5494
5728
|
return null;
|
|
5495
|
-
const
|
|
5729
|
+
const wholeGi = whole ? resolveSegmentedDateGroup(instruction, enumerated.groups) : null;
|
|
5730
|
+
const partStep = wholeGi === null ? part : null;
|
|
5731
|
+
const value = (wholeGi !== null ? whole : part);
|
|
5732
|
+
const partOf = (group) => partStep ? segmentIndexForPart(group, partStep.part) : null;
|
|
5733
|
+
const partFits = (group) => {
|
|
5734
|
+
const si = partOf(group);
|
|
5735
|
+
return (!group.nested &&
|
|
5736
|
+
si !== null &&
|
|
5737
|
+
value.digits.length <= String(group.segmentMaxes[si] ?? 0).length);
|
|
5738
|
+
};
|
|
5739
|
+
const gi = wholeGi ??
|
|
5740
|
+
(partStep
|
|
5741
|
+
? pickSegmentedDateGroup(instruction, enumerated.groups, partStep, partFits)
|
|
5742
|
+
: null);
|
|
5496
5743
|
if (gi === null) {
|
|
5497
5744
|
logger.info("segmented date primitive: no unambiguous group; falling through");
|
|
5498
5745
|
await target.evaluate(SEGMENTED_DATE_CLEAR_EXPR);
|
|
@@ -5500,10 +5747,15 @@ async function trySegmentedDateSpinbuttonPrimitive(params) {
|
|
|
5500
5747
|
}
|
|
5501
5748
|
const maxes = enumerated.groups[gi]?.segmentMaxes ?? [];
|
|
5502
5749
|
const widths = maxes.map((max) => String(max).length);
|
|
5503
|
-
const
|
|
5504
|
-
|
|
5505
|
-
|
|
5506
|
-
|
|
5750
|
+
const onlySegment = partStep ? partOf(enumerated.groups[gi]) : null;
|
|
5751
|
+
const active = widths.flatMap((_, si) => onlySegment === null || si === onlySegment ? [si] : []);
|
|
5752
|
+
const sliceFor = (si) => {
|
|
5753
|
+
if (onlySegment !== null)
|
|
5754
|
+
return value.digits;
|
|
5755
|
+
const start = widths.slice(0, si).reduce((a, b) => a + b, 0);
|
|
5756
|
+
return value.digits.slice(start, start + (widths[si] ?? 0));
|
|
5757
|
+
};
|
|
5758
|
+
const expected = active.map((si) => Number(sliceFor(si)));
|
|
5507
5759
|
const segmentSelector = (si) => `[${SEGMENTED_DATE_MARKER_ATTR}~="${gi}:${si}"]`;
|
|
5508
5760
|
const focusSegment = async (si) => {
|
|
5509
5761
|
const selector = segmentSelector(si);
|
|
@@ -5516,7 +5768,9 @@ async function trySegmentedDateSpinbuttonPrimitive(params) {
|
|
|
5516
5768
|
// scrolling is best-effort; the key events only need the segment focused
|
|
5517
5769
|
}
|
|
5518
5770
|
try {
|
|
5519
|
-
|
|
5771
|
+
// An open option popup can cover the field; clear it so the trusted
|
|
5772
|
+
// click lands on the segment instead of falling back to a bare focus().
|
|
5773
|
+
if (await clearObscuringPopup(page, target, selector)) {
|
|
5520
5774
|
await (0, trusted_click_1.clickCentredTrusted)(target, selector, { label: "date segment" });
|
|
5521
5775
|
}
|
|
5522
5776
|
}
|
|
@@ -5536,12 +5790,9 @@ async function trySegmentedDateSpinbuttonPrimitive(params) {
|
|
|
5536
5790
|
await sendCDP("Input.dispatchKeyEvent", { type: "keyUp", ...keyArgs });
|
|
5537
5791
|
}
|
|
5538
5792
|
};
|
|
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
|
-
});
|
|
5793
|
+
const segmentDigits = active.map((si) => sliceFor(si));
|
|
5543
5794
|
const readSegments = async () => (await target.evaluate(`(() => {
|
|
5544
|
-
const sels = ${JSON.stringify(
|
|
5795
|
+
const sels = ${JSON.stringify(active.map((si) => segmentSelector(si)))};
|
|
5545
5796
|
return sels.map((sel) => {
|
|
5546
5797
|
const s = document.querySelector(sel);
|
|
5547
5798
|
if (!s) return NaN;
|
|
@@ -5558,15 +5809,18 @@ async function trySegmentedDateSpinbuttonPrimitive(params) {
|
|
|
5558
5809
|
// locator.type (per-character key events, never Input.insertText) covers a
|
|
5559
5810
|
// page whose CDP channel does not reach the widget.
|
|
5560
5811
|
const typeViaLocator = async () => {
|
|
5561
|
-
for (const [
|
|
5562
|
-
await target
|
|
5812
|
+
for (const [k, digits] of segmentDigits.entries()) {
|
|
5813
|
+
await target
|
|
5814
|
+
.locator(segmentSelector(active[k] ?? 0))
|
|
5815
|
+
.first()
|
|
5816
|
+
.type(digits, {
|
|
5563
5817
|
delay: SEGMENTED_DATE_KEY_DELAY_MS,
|
|
5564
5818
|
});
|
|
5565
5819
|
}
|
|
5566
5820
|
};
|
|
5567
5821
|
const typeViaKeyEvents = async () => {
|
|
5568
|
-
for (const [
|
|
5569
|
-
await focusSegment(
|
|
5822
|
+
for (const [k, digits] of segmentDigits.entries()) {
|
|
5823
|
+
await focusSegment(active[k] ?? 0);
|
|
5570
5824
|
await typeViaCdp(digits);
|
|
5571
5825
|
}
|
|
5572
5826
|
};
|
|
@@ -5961,6 +6215,16 @@ function isNavigatingEvaluateRejection(error) {
|
|
|
5961
6215
|
message.includes("no frame for given id found") ||
|
|
5962
6216
|
GENERIC_CONTENT_FREE_EVAL_REJECTION_MESSAGES.has(message.trim()));
|
|
5963
6217
|
}
|
|
6218
|
+
/**
|
|
6219
|
+
* Settles after a click and returns the post-click snapshot, so the settle
|
|
6220
|
+
* wait and the phantom classifier decide from the same reader and baseline.
|
|
6221
|
+
* Without a page there is nothing to wait on and the snapshot is taken as-is.
|
|
6222
|
+
*/
|
|
6223
|
+
async function settleAndSnapshotPostClick(target, signalCounter, page, pre) {
|
|
6224
|
+
if (page)
|
|
6225
|
+
await settleWithPageQuiescence(page, target, signalCounter, pre.networkCount, true, pre.visibleTextSignature);
|
|
6226
|
+
return snapshotPage(target, signalCounter, page);
|
|
6227
|
+
}
|
|
5964
6228
|
/**
|
|
5965
6229
|
* Shared in-page discovery of the widget's callback, used identically by
|
|
5966
6230
|
* `injectCaptchaTokenAndSubmit`'s precheck (to decide the branch) and its
|
|
@@ -6268,9 +6532,7 @@ async function submitCaptchaGatedForm(target, responseField = "h-captcha-respons
|
|
|
6268
6532
|
if (clickResult.clicked) {
|
|
6269
6533
|
if (!pre || !verification)
|
|
6270
6534
|
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);
|
|
6535
|
+
const post = await settleAndSnapshotPostClick(target, verification.signalCounter, verification.page, pre);
|
|
6274
6536
|
const verdict = (0, phantom_click_1.classifyPhantomClick)({
|
|
6275
6537
|
actResultSuccess: true,
|
|
6276
6538
|
pre,
|
|
@@ -6291,9 +6553,7 @@ async function submitCaptchaGatedForm(target, responseField = "h-captcha-respons
|
|
|
6291
6553
|
throw err;
|
|
6292
6554
|
});
|
|
6293
6555
|
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);
|
|
6556
|
+
const runnerUpPost = await settleAndSnapshotPostClick(target, verification.signalCounter, verification.page, post);
|
|
6297
6557
|
const runnerUpVerdict = (0, phantom_click_1.classifyPhantomClick)({
|
|
6298
6558
|
actResultSuccess: true,
|
|
6299
6559
|
pre: post,
|
|
@@ -6839,7 +7099,10 @@ async function tryCheckboxStatePrimitive(params) {
|
|
|
6839
7099
|
let overlaid = false;
|
|
6840
7100
|
let trustedDelivered = false;
|
|
6841
7101
|
for (const sel of clickSels) {
|
|
6842
|
-
const
|
|
7102
|
+
const firstProbe = await (0, trusted_click_1.probeClickPoint)(target, sel);
|
|
7103
|
+
const probe = firstProbe?.state === "occluded" && firstProbe.occluder !== "none"
|
|
7104
|
+
? await dismissLeftoverPromptPopups(page, target).then(() => (0, trusted_click_1.probeClickPoint)(target, sel), () => firstProbe)
|
|
7105
|
+
: firstProbe;
|
|
6843
7106
|
if (probe?.state === "occluded" && probe.occluder !== "none") {
|
|
6844
7107
|
overlaid = true;
|
|
6845
7108
|
continue;
|
|
@@ -7897,7 +8160,7 @@ async function tryPromptSelectorPrimitive(params) {
|
|
|
7897
8160
|
logger.info(`prompt-selector primitive: filtered list for ${optLabel} names no matching option; submitting with Enter`);
|
|
7898
8161
|
try {
|
|
7899
8162
|
await target.evaluate(`(() => { const el = document.querySelector('[${PROMPT_OPTION_MARK_ATTR}-search="1"]'); if (el) el.focus(); })()`);
|
|
7900
|
-
await sendTrustedEnter(page);
|
|
8163
|
+
await sendTrustedEnter(page, target);
|
|
7901
8164
|
}
|
|
7902
8165
|
catch (err) {
|
|
7903
8166
|
logger.info(`prompt-selector primitive: enter submit failed for ${optLabel}: ${(0, errors_1.toErrorMessage)(err)}`);
|
|
@@ -7940,7 +8203,7 @@ const PROMPT_SELECTOR_MAX_STABLE_READS = 5;
|
|
|
7940
8203
|
* committer is never handed an unsettled list.
|
|
7941
8204
|
*/
|
|
7942
8205
|
async function readStablePromptOptions(page, target, enumerateOptionsExpr, initial) {
|
|
7943
|
-
const key = (r) => JSON.stringify((r?.options ?? []).map((o) => [o.text, o.
|
|
8206
|
+
const key = (r) => JSON.stringify((r?.options ?? []).map((o) => [o.text, o.domText ?? null]));
|
|
7944
8207
|
let current = initial;
|
|
7945
8208
|
for (let read = 0; read < PROMPT_SELECTOR_MAX_STABLE_READS; read++) {
|
|
7946
8209
|
await page.waitForTimeout(PROMPT_SELECTOR_SETTLE_MS);
|
|
@@ -7979,33 +8242,78 @@ function promptOptionsNameOption(result, option) {
|
|
|
7979
8242
|
const want = normPromptOptionText(option);
|
|
7980
8243
|
if (want === "")
|
|
7981
8244
|
return true;
|
|
7982
|
-
return (result?.options ?? []).some((o) => {
|
|
8245
|
+
return realPromptOptions(result?.options ?? [], option).some((o) => {
|
|
8246
|
+
const text = normPromptOptionText(o.text);
|
|
8247
|
+
return text === want || text.includes(want);
|
|
8248
|
+
});
|
|
8249
|
+
}
|
|
8250
|
+
/**
|
|
8251
|
+
* Single classification of which rendered entries are real options, shared by
|
|
8252
|
+
* the submit decision and the committer so they cannot disagree. Placeholders
|
|
8253
|
+
* ("No Items.", "No results for x") are dropped, except an entry that is
|
|
8254
|
+
* exactly the requested text, which is the user's own option.
|
|
8255
|
+
*/
|
|
8256
|
+
function realPromptOptions(options, option) {
|
|
8257
|
+
const want = normPromptOptionText(option);
|
|
8258
|
+
return options.filter((o) => {
|
|
7983
8259
|
const text = normPromptOptionText(o.text);
|
|
7984
8260
|
if (text === "")
|
|
7985
8261
|
return false;
|
|
7986
|
-
|
|
7987
|
-
return true;
|
|
7988
|
-
if (PROMPT_PLACEHOLDER_RE.test(text))
|
|
7989
|
-
return false;
|
|
7990
|
-
return text.includes(want);
|
|
8262
|
+
return text === want || !PROMPT_PLACEHOLDER_RE.test(text);
|
|
7991
8263
|
});
|
|
7992
8264
|
}
|
|
7993
|
-
/**
|
|
7994
|
-
|
|
7995
|
-
|
|
7996
|
-
|
|
7997
|
-
|
|
8265
|
+
/**
|
|
8266
|
+
* Single owner of CDP delivery for trusted input: a child-frame target's own
|
|
8267
|
+
* session when it has one, otherwise the page's, so keys reach the frame that
|
|
8268
|
+
* holds the focused element.
|
|
8269
|
+
*/
|
|
8270
|
+
function frameCdpSender(page, target) {
|
|
8271
|
+
return (method, args) => target.frame?.session ? target.frame.session.send(method, args) : page.sendCDP(method, args);
|
|
7998
8272
|
}
|
|
7999
|
-
/**
|
|
8000
|
-
|
|
8001
|
-
|
|
8273
|
+
/**
|
|
8274
|
+
* Clicks at a frame-local viewport point through the same CDP route as keys, so
|
|
8275
|
+
* the press lands where the hit test just ran instead of re-resolving a locator.
|
|
8276
|
+
* A same-process child frame has no session of its own, so its offset in the
|
|
8277
|
+
* page is added to reach page coordinates.
|
|
8278
|
+
*/
|
|
8279
|
+
async function dispatchTrustedClick(page, target, x, y) {
|
|
8280
|
+
if (target.frame === null) {
|
|
8281
|
+
await page.click(x, y);
|
|
8282
|
+
return;
|
|
8283
|
+
}
|
|
8284
|
+
const send = frameCdpSender(page, target);
|
|
8285
|
+
const offset = target.frame.session
|
|
8286
|
+
? { x: 0, y: 0 }
|
|
8287
|
+
: ((await target
|
|
8288
|
+
.evaluate(`(() => {
|
|
8289
|
+
const f = window.frameElement;
|
|
8290
|
+
if (!f) return null;
|
|
8291
|
+
const r = f.getBoundingClientRect();
|
|
8292
|
+
return { x: r.left, y: r.top };
|
|
8293
|
+
})()`)
|
|
8294
|
+
.catch(() => null)) ?? { x: 0, y: 0 });
|
|
8295
|
+
const point = { x: x + offset.x, y: y + offset.y, button: "left", clickCount: 1 };
|
|
8296
|
+
await send("Input.dispatchMouseEvent", { type: "mouseMoved", x: point.x, y: point.y });
|
|
8297
|
+
await send("Input.dispatchMouseEvent", { type: "mousePressed", ...point });
|
|
8298
|
+
await send("Input.dispatchMouseEvent", { type: "mouseReleased", ...point });
|
|
8299
|
+
}
|
|
8300
|
+
async function sendTrustedKey(page, target, key, text) {
|
|
8301
|
+
const send = frameCdpSender(page, target);
|
|
8302
|
+
await send("Input.dispatchKeyEvent", { type: "keyDown", ...(text ? { text } : {}), ...key });
|
|
8303
|
+
await send("Input.dispatchKeyEvent", { type: "keyUp", ...key });
|
|
8304
|
+
}
|
|
8305
|
+
/** Dispatches a trusted Enter to the focused element (the filter input) via the target frame's CDP session. */
|
|
8306
|
+
async function sendTrustedEnter(page, target) {
|
|
8307
|
+
await sendTrustedKey(page, target, { key: "Enter", code: "Enter", windowsVirtualKeyCode: 13, nativeVirtualKeyCode: 13 }, "\r");
|
|
8308
|
+
}
|
|
8309
|
+
/** Dispatches a trusted Escape to the focused element via the target frame's CDP session, closing a popup left open. */
|
|
8310
|
+
async function sendTrustedEscape(page, target) {
|
|
8311
|
+
await sendTrustedKey(page, target, {
|
|
8002
8312
|
key: "Escape",
|
|
8003
8313
|
code: "Escape",
|
|
8004
8314
|
windowsVirtualKeyCode: 27,
|
|
8005
8315
|
nativeVirtualKeyCode: 27,
|
|
8006
|
-
};
|
|
8007
|
-
await page.sendCDP("Input.dispatchKeyEvent", { type: "keyDown", ...key });
|
|
8008
|
-
await page.sendCDP("Input.dispatchKeyEvent", { type: "keyUp", ...key });
|
|
8316
|
+
});
|
|
8009
8317
|
}
|
|
8010
8318
|
/**
|
|
8011
8319
|
* Counts rendered listbox / activeListContainer popups other than the chip
|
|
@@ -8049,18 +8357,52 @@ async function dismissLeftoverPromptPopups(page, target) {
|
|
|
8049
8357
|
.catch(() => 0));
|
|
8050
8358
|
if (!count)
|
|
8051
8359
|
return;
|
|
8052
|
-
logger.info(`
|
|
8053
|
-
const trusted = await sendTrustedEscape(page)
|
|
8360
|
+
logger.info(`popup dismissal: dismissing ${count} leftover popup(s) via Escape`);
|
|
8361
|
+
const trusted = await sendTrustedEscape(page, target)
|
|
8054
8362
|
.then(() => true)
|
|
8055
8363
|
.catch((err) => {
|
|
8056
|
-
logger.info(`
|
|
8364
|
+
logger.info(`popup dismissal: Escape dispatch failed: ${(0, errors_1.toErrorMessage)(err)}`);
|
|
8057
8365
|
return false;
|
|
8058
8366
|
});
|
|
8059
8367
|
if (!trusted || attempt > 0)
|
|
8060
8368
|
await target.evaluate(IN_PAGE_ESCAPE_EXPR).catch(() => undefined);
|
|
8061
|
-
await page
|
|
8369
|
+
await settleAfterEscape(page);
|
|
8062
8370
|
}
|
|
8063
8371
|
}
|
|
8372
|
+
/** Waits for a dismissed popup to unpaint; a page without a timer seam just proceeds. */
|
|
8373
|
+
async function settleAfterEscape(page) {
|
|
8374
|
+
await Promise.resolve()
|
|
8375
|
+
.then(() => page.waitForTimeout(PROMPT_SELECTOR_SETTLE_MS))
|
|
8376
|
+
.catch(() => undefined);
|
|
8377
|
+
}
|
|
8378
|
+
/**
|
|
8379
|
+
* Clears whatever popup paints over `selector`'s click point so the caller's
|
|
8380
|
+
* trusted click lands on the field. The hit test, not the popup's markup, is
|
|
8381
|
+
* the evidence: a named occluder gets the listbox-aware dismissal first and
|
|
8382
|
+
* then a plain Escape for popups that dismissal does not recognise. Resolves
|
|
8383
|
+
* true only when a fresh hit test reaches the field; an indeterminate probe
|
|
8384
|
+
* (no layout, nothing at the point) only ever gets the listbox-aware
|
|
8385
|
+
* dismissal, never a blind Escape.
|
|
8386
|
+
*/
|
|
8387
|
+
async function clearObscuringPopup(page, target, selector) {
|
|
8388
|
+
const covered = (probe) => probe?.state === "occluded" && probe.occluder !== undefined && probe.occluder !== "none";
|
|
8389
|
+
const first = await (0, trusted_click_1.probeClickPoint)(target, selector);
|
|
8390
|
+
if (first?.state === "hit")
|
|
8391
|
+
return true;
|
|
8392
|
+
await dismissLeftoverPromptPopups(page, target).catch(() => undefined);
|
|
8393
|
+
const afterListbox = await (0, trusted_click_1.probeClickPoint)(target, selector);
|
|
8394
|
+
if (afterListbox?.state === "hit")
|
|
8395
|
+
return true;
|
|
8396
|
+
if (!covered(afterListbox))
|
|
8397
|
+
return false;
|
|
8398
|
+
const trusted = await sendTrustedEscape(page, target)
|
|
8399
|
+
.then(() => true)
|
|
8400
|
+
.catch(() => false);
|
|
8401
|
+
if (!trusted)
|
|
8402
|
+
await target.evaluate(IN_PAGE_ESCAPE_EXPR).catch(() => undefined);
|
|
8403
|
+
await settleAfterEscape(page);
|
|
8404
|
+
return (await (0, trusted_click_1.probeClickPoint)(target, selector))?.state === "hit";
|
|
8405
|
+
}
|
|
8064
8406
|
/**
|
|
8065
8407
|
* Phase 3 of {@link tryPromptSelectorPrimitive}: given the popup's rendered
|
|
8066
8408
|
* (possibly filtered) option list, match the requested option — deterministic
|
|
@@ -8091,7 +8433,7 @@ async function commitPromptOption(params) {
|
|
|
8091
8433
|
.join("");
|
|
8092
8434
|
let optionsResult = params.optionsResult;
|
|
8093
8435
|
for (let depth = 0; depth <= PROMPT_SELECTOR_MAX_DRILL_DEPTH; depth++) {
|
|
8094
|
-
const options = optionsResult.options ?? [];
|
|
8436
|
+
const options = realPromptOptions(optionsResult.options ?? [], option);
|
|
8095
8437
|
const detMatch = options.find((o) => norm(o.text) === wantOpt) ?? null;
|
|
8096
8438
|
if (detMatch === null && (anthropic === null || options.length === 0)) {
|
|
8097
8439
|
logger.info(`prompt-selector primitive: no option match for ${optLabel}${anthropic === null ? " (no LLM client)" : ""}; falling through to cascade`);
|
|
@@ -8129,7 +8471,7 @@ async function commitPromptOption(params) {
|
|
|
8129
8471
|
let clicked = false;
|
|
8130
8472
|
for (let attempt = 0; attempt < PROMPT_SELECTOR_MAX_CLICK_RESOLVES && !clicked; attempt++) {
|
|
8131
8473
|
const fresh = (await target.evaluate(enumerateOptionsExpr).catch(() => null));
|
|
8132
|
-
const freshMatches = (fresh?.options ?? []).filter((o) => norm(o.text) === wantChosenText);
|
|
8474
|
+
const freshMatches = realPromptOptions(fresh?.options ?? [], option).filter((o) => norm(o.text) === wantChosenText);
|
|
8133
8475
|
const freshOption = freshMatches.find((o) => o.nodeId !== undefined && o.nodeId === chosenOption.nodeId) ??
|
|
8134
8476
|
freshMatches[0];
|
|
8135
8477
|
if (!freshOption) {
|
|
@@ -8152,6 +8494,19 @@ async function commitPromptOption(params) {
|
|
|
8152
8494
|
await (0, trusted_click_1.clickCentredTrusted)(target, optionSel, {
|
|
8153
8495
|
label: "prompt-selector option",
|
|
8154
8496
|
expectText: expectedDomText,
|
|
8497
|
+
clickAtPoint: async (x, y) => {
|
|
8498
|
+
const stillThere = await target
|
|
8499
|
+
.evaluate(`((x, y, want) => {
|
|
8500
|
+
const hit = document.elementFromPoint(x, y);
|
|
8501
|
+
const el = hit && hit.closest(${JSON.stringify(optionSel)});
|
|
8502
|
+
return !!el && (el.textContent || "").replace(/\\s+/g, " ").trim() === want;
|
|
8503
|
+
})(${x}, ${y}, ${JSON.stringify(expectedDomText)})`)
|
|
8504
|
+
.catch(() => false);
|
|
8505
|
+
if (!stillThere) {
|
|
8506
|
+
throw new trusted_click_1.CentredClickError("centred click: prompt-selector option identity mismatch at click point, click withheld", "mismatch");
|
|
8507
|
+
}
|
|
8508
|
+
await dispatchTrustedClick(page, target, x, y);
|
|
8509
|
+
},
|
|
8155
8510
|
verifyBeforeClick: async () => (await target
|
|
8156
8511
|
.evaluate(`((sel, want) => {
|
|
8157
8512
|
const el = document.querySelector(sel);
|
|
@@ -8180,6 +8535,7 @@ async function commitPromptOption(params) {
|
|
|
8180
8535
|
const emptyRx = new RegExp(emptyRxSrc, emptyRxFlags);
|
|
8181
8536
|
const currentText = ${PROMPT_CURRENT_TEXT_EXPR};
|
|
8182
8537
|
const announcedValue = ${PROMPT_ANNOUNCED_VALUE_EXPR};
|
|
8538
|
+
const namesOption = ${PROMPT_NAMES_OPTION_EXPR};
|
|
8183
8539
|
const w = document.querySelector("[" + markAttr + '="' + wIdx + '"]');
|
|
8184
8540
|
if (!w) return { ok: false, id: "", stillInvalid: false };
|
|
8185
8541
|
const text = currentText(w, valueSel, emptyRx);
|
|
@@ -8238,7 +8594,7 @@ async function commitPromptOption(params) {
|
|
|
8238
8594
|
// The widget's own announcement of what it committed is ground truth when readable.
|
|
8239
8595
|
const committed = announcedValue(w);
|
|
8240
8596
|
if (committed) {
|
|
8241
|
-
return { ok: !!wantText && committed
|
|
8597
|
+
return { ok: !!wantText && namesOption(committed, wantText), id: w.id || "", stillInvalid, committed };
|
|
8242
8598
|
}
|
|
8243
8599
|
// A wrong option leaves the widget valid, so !stillInvalid proves nothing
|
|
8244
8600
|
// once the widget's own value is readable: a readable value naming another
|
|
@@ -9520,7 +9876,7 @@ async function probeChildFrameSubmitFallback(params) {
|
|
|
9520
9876
|
await (0, click_settle_1.settleAfterClick)(page, signalCounter, pre.networkCount, true, async () => {
|
|
9521
9877
|
const snap = await snapshotPage(target, signalCounter);
|
|
9522
9878
|
return snap.visibleTextSignature;
|
|
9523
|
-
}, pre.visibleTextSignature);
|
|
9879
|
+
}, pre.visibleTextSignature, async () => target.evaluate(click_settle_1.QUIESCENCE_PROBE_EXPR));
|
|
9524
9880
|
const post = await snapshotPage(target, signalCounter, page);
|
|
9525
9881
|
const verdict = (0, phantom_click_1.classifyPhantomClick)({
|
|
9526
9882
|
actResultSuccess: true,
|
|
@@ -9541,7 +9897,7 @@ async function probeChildFrameSubmitFallback(params) {
|
|
|
9541
9897
|
await (0, click_settle_1.settleAfterClick)(page, signalCounter, post.networkCount, true, async () => {
|
|
9542
9898
|
const snap = await snapshotPage(target, signalCounter);
|
|
9543
9899
|
return snap.visibleTextSignature;
|
|
9544
|
-
}, post.visibleTextSignature);
|
|
9900
|
+
}, post.visibleTextSignature, async () => target.evaluate(click_settle_1.QUIESCENCE_PROBE_EXPR));
|
|
9545
9901
|
const runnerUpPost = await snapshotPage(target, signalCounter, page);
|
|
9546
9902
|
const runnerUpVerdict = (0, phantom_click_1.classifyPhantomClick)({
|
|
9547
9903
|
actResultSuccess: true,
|
|
@@ -10299,7 +10655,7 @@ async function executeStepWithHealing(params) {
|
|
|
10299
10655
|
});
|
|
10300
10656
|
if (backendErrorUrl !== null) {
|
|
10301
10657
|
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.
|
|
10658
|
+
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
10659
|
}
|
|
10304
10660
|
// Bounded same-origin child-iframe fallback: the probe above only ever
|
|
10305
10661
|
// looked at the resolved (declared-or-main) frame's own light+shadow DOM,
|
|
@@ -10384,7 +10740,7 @@ async function executeStepWithHealing(params) {
|
|
|
10384
10740
|
targetResolutionDiagnostic,
|
|
10385
10741
|
fieldValuesAtFailure,
|
|
10386
10742
|
}) ?? null;
|
|
10387
|
-
throw new errors_2.StepVerificationError(`${formatStepPrefix(stepIndex, totalSteps)} (${(0, pii_redaction_1.
|
|
10743
|
+
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
10744
|
}
|
|
10389
10745
|
// Pre-submit form-validity probe. Fires on the canonical submit step
|
|
10390
10746
|
// (either the final flow step OR a step explicitly flagged `submitStep:
|
|
@@ -10766,13 +11122,13 @@ async function executeStepWithHealing(params) {
|
|
|
10766
11122
|
// control stays covered so the attempt fails instead of clicking an overlay.
|
|
10767
11123
|
// Resolves true once the observe ran, so guardedAct skips its own pass.
|
|
10768
11124
|
const preCentreNaturalLanguageClick = async (instruction) => {
|
|
10769
|
-
if (!stagehand_guard_1.
|
|
11125
|
+
if (!(0, stagehand_guard_1.hasClickIntent)(instruction))
|
|
10770
11126
|
return false;
|
|
10771
|
-
const observed = await (0, stagehand_guard_1.guardedObserve)(stagehand, instruction, { timeout: exports.STEP_WATCHDOG_MS }, captureFn, frameTarget)
|
|
11127
|
+
const observed = await (0, stagehand_guard_1.guardedObserve)(stagehand, instruction, { timeout: exports.STEP_WATCHDOG_MS }, captureFn, frameTarget);
|
|
10772
11128
|
const first = observed[0];
|
|
10773
|
-
if (first?.method
|
|
11129
|
+
if (!(0, stagehand_guard_1.isTrustedClickMethod)(first?.method) || !first?.selector)
|
|
10774
11130
|
return true;
|
|
10775
|
-
const failure = await centreObservedClickTarget(frameTarget ?? (0, frame_target_1.mainFrameTarget)(page), first.selector
|
|
11131
|
+
const failure = await centreObservedClickTarget(frameTarget ?? (0, frame_target_1.mainFrameTarget)(page), first.selector);
|
|
10776
11132
|
if (failure)
|
|
10777
11133
|
throw new Error(failure);
|
|
10778
11134
|
return true;
|
|
@@ -10951,11 +11307,15 @@ async function executeStepWithHealing(params) {
|
|
|
10951
11307
|
// burn the step budget probing all of them.
|
|
10952
11308
|
const runnerUp = ranked[1];
|
|
10953
11309
|
if (runnerUp) {
|
|
10954
|
-
|
|
10955
|
-
|
|
10956
|
-
|
|
10957
|
-
|
|
10958
|
-
|
|
11310
|
+
const midTarget = frameTarget ?? (0, frame_target_1.mainFrameTarget)(page);
|
|
11311
|
+
const midPost = await (0, click_settle_1.settleAndSnapshot)({
|
|
11312
|
+
page,
|
|
11313
|
+
counter: signalCounter,
|
|
11314
|
+
pre,
|
|
11315
|
+
submitShaped: true,
|
|
11316
|
+
snapshot: (withPage) => snapshotPage(midTarget, signalCounter, withPage ? page : undefined),
|
|
11317
|
+
readQuiescence: async () => midTarget.evaluate(click_settle_1.QUIESCENCE_PROBE_EXPR),
|
|
11318
|
+
});
|
|
10959
11319
|
const topVerdict = (0, phantom_click_1.classifyPhantomClick)({
|
|
10960
11320
|
actResultSuccess: true,
|
|
10961
11321
|
pre,
|
|
@@ -11370,8 +11730,8 @@ async function executeStepWithHealing(params) {
|
|
|
11370
11730
|
// bars, so centre + hit-test the observed control first; an
|
|
11371
11731
|
// unrecoverable occlusion fails the attempt instead of clicking an
|
|
11372
11732
|
// overlay.
|
|
11373
|
-
if (overriddenTarget.method
|
|
11374
|
-
const centreFailure = await centreObservedClickTarget(frameTarget ?? (0, frame_target_1.mainFrameTarget)(page), overriddenTarget.selector
|
|
11733
|
+
if ((0, stagehand_guard_1.isTrustedClickMethod)(overriddenTarget.method)) {
|
|
11734
|
+
const centreFailure = await centreObservedClickTarget(frameTarget ?? (0, frame_target_1.mainFrameTarget)(page), overriddenTarget.selector);
|
|
11375
11735
|
if (centreFailure) {
|
|
11376
11736
|
record.errorMessage = centreFailure;
|
|
11377
11737
|
attempts.push(record);
|
|
@@ -11716,10 +12076,7 @@ async function executeStepWithHealing(params) {
|
|
|
11716
12076
|
: !STATE_CLASS_METHODS.has(resolvedAction.method ?? "");
|
|
11717
12077
|
if (mayTriggerRoundTrip) {
|
|
11718
12078
|
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);
|
|
12079
|
+
await settleWithPageQuiescence(page, settleTarget, signalCounter, pre.networkCount, isSettleWaitStep, pre.visibleTextSignature);
|
|
11723
12080
|
}
|
|
11724
12081
|
const post = await snapshotPage(frameTarget ?? (0, frame_target_1.mainFrameTarget)(page), signalCounter, page);
|
|
11725
12082
|
record.post = post;
|
|
@@ -12383,10 +12740,7 @@ async function executeStepWithHealing(params) {
|
|
|
12383
12740
|
probeResult.checked === true &&
|
|
12384
12741
|
(probeResult.ariaManaged === true || !ancestorStillInvalid);
|
|
12385
12742
|
await page.waitForTimeout(STEP_PAUSE_MS);
|
|
12386
|
-
await (0,
|
|
12387
|
-
const snap = await snapshotPage(frameTarget ?? (0, frame_target_1.mainFrameTarget)(page), signalCounter);
|
|
12388
|
-
return snap.visibleTextSignature;
|
|
12389
|
-
}, pre.visibleTextSignature);
|
|
12743
|
+
await settleWithPageQuiescence(page, frameTarget ?? (0, frame_target_1.mainFrameTarget)(page), signalCounter, pre.networkCount, isSettleWaitStep, pre.visibleTextSignature);
|
|
12390
12744
|
const retryPost = await snapshotPage(frameTarget ?? (0, frame_target_1.mainFrameTarget)(page), signalCounter, page);
|
|
12391
12745
|
const retryNetworkFired = retryPost.networkCount > pre.networkCount;
|
|
12392
12746
|
const retryMidFlowSubmit = isMidFlowSubmitShapedStep({
|
|
@@ -13093,7 +13447,7 @@ async function executeStepWithHealing(params) {
|
|
|
13093
13447
|
if (dumpPath !== null) {
|
|
13094
13448
|
logger.error(`${formatStepPrefix(stepIndex, totalSteps)} failed after ${MAX_STEP_ATTEMPTS} attempts; diagnostic bundle: ${dumpPath}`);
|
|
13095
13449
|
}
|
|
13096
|
-
throw new errors_2.StepVerificationError(`${formatStepPrefix(stepIndex, totalSteps)} (${(0, pii_redaction_1.
|
|
13450
|
+
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
13451
|
}
|
|
13098
13452
|
/**
|
|
13099
13453
|
* Navigates `page` directly to `url` and waits for the SPA to hydrate,
|