@enricai/barnacle 1.6.12 → 1.6.13
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/README.md +113 -9
- package/dist/api/schemas/submissions.d.ts +14 -0
- package/dist/api/schemas/submissions.d.ts.map +1 -1
- package/dist/api/schemas/submissions.js +1 -0
- package/dist/api/schemas/submissions.js.map +1 -1
- package/dist/config.d.ts +33 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +4 -0
- package/dist/config.js.map +1 -1
- package/dist/lib/telemetry/beacon-capture.d.ts +45 -2
- package/dist/lib/telemetry/beacon-capture.d.ts.map +1 -1
- package/dist/lib/telemetry/beacon-capture.js +36 -0
- package/dist/lib/telemetry/beacon-capture.js.map +1 -1
- package/dist/lib/telemetry/reconciliation-record.d.ts +41 -1
- package/dist/lib/telemetry/reconciliation-record.d.ts.map +1 -1
- package/dist/lib/telemetry/reconciliation-record.js +29 -2
- package/dist/lib/telemetry/reconciliation-record.js.map +1 -1
- package/dist/lib/telemetry/reconciliation-source.d.ts +7 -1
- package/dist/lib/telemetry/reconciliation-source.d.ts.map +1 -1
- package/dist/lib/telemetry/reconciliation-source.js +21 -5
- package/dist/lib/telemetry/reconciliation-source.js.map +1 -1
- package/dist/lib/telemetry/run-telemetry.d.ts +67 -0
- package/dist/lib/telemetry/run-telemetry.d.ts.map +1 -0
- package/dist/lib/telemetry/run-telemetry.js +55 -0
- package/dist/lib/telemetry/run-telemetry.js.map +1 -0
- package/dist/lib/telemetry/submission-capture.d.ts +11 -4
- package/dist/lib/telemetry/submission-capture.d.ts.map +1 -1
- package/dist/lib/telemetry/submission-capture.js +1 -0
- package/dist/lib/telemetry/submission-capture.js.map +1 -1
- package/dist/lib/telemetry/submission-reader.d.ts +11 -3
- package/dist/lib/telemetry/submission-reader.d.ts.map +1 -1
- package/dist/lib/telemetry/submission-reader.js +32 -8
- package/dist/lib/telemetry/submission-reader.js.map +1 -1
- package/dist/lib/tracking-click.d.ts.map +1 -1
- package/dist/lib/tracking-click.js +19 -0
- package/dist/lib/tracking-click.js.map +1 -1
- package/dist/plugins/loader.d.ts +20 -0
- package/dist/plugins/loader.d.ts.map +1 -1
- package/dist/plugins/loader.js +94 -17
- package/dist/plugins/loader.js.map +1 -1
- package/dist/scraper/deep-locator-actuate.d.ts +94 -0
- package/dist/scraper/deep-locator-actuate.d.ts.map +1 -0
- package/dist/scraper/deep-locator-actuate.js +216 -0
- package/dist/scraper/deep-locator-actuate.js.map +1 -0
- package/dist/scraper/deep-locator-candidates.d.ts +105 -18
- package/dist/scraper/deep-locator-candidates.d.ts.map +1 -1
- package/dist/scraper/deep-locator-candidates.js +238 -46
- package/dist/scraper/deep-locator-candidates.js.map +1 -1
- package/dist/scraper/deep-locator-click.d.ts +71 -0
- package/dist/scraper/deep-locator-click.d.ts.map +1 -0
- package/dist/scraper/deep-locator-click.js +71 -0
- package/dist/scraper/deep-locator-click.js.map +1 -0
- package/dist/scraper/deep-locator-fake.d.ts +344 -14
- package/dist/scraper/deep-locator-fake.d.ts.map +1 -1
- package/dist/scraper/deep-locator-fake.js +509 -7
- package/dist/scraper/deep-locator-fake.js.map +1 -1
- package/dist/scraper/deep-locator-scan.d.ts +220 -0
- package/dist/scraper/deep-locator-scan.d.ts.map +1 -0
- package/dist/scraper/deep-locator-scan.js +386 -0
- package/dist/scraper/deep-locator-scan.js.map +1 -0
- package/dist/scraper/fake-timer-advance.d.ts +47 -0
- package/dist/scraper/fake-timer-advance.d.ts.map +1 -0
- package/dist/scraper/fake-timer-advance.js +44 -0
- package/dist/scraper/fake-timer-advance.js.map +1 -0
- package/dist/scraper/flow-runner.d.ts +36 -1
- package/dist/scraper/flow-runner.d.ts.map +1 -1
- package/dist/scraper/flow-runner.js +354 -56
- package/dist/scraper/flow-runner.js.map +1 -1
- package/dist/scraper/frame-target.d.ts +28 -0
- package/dist/scraper/frame-target.d.ts.map +1 -1
- package/dist/scraper/frame-target.js +30 -0
- package/dist/scraper/frame-target.js.map +1 -1
- package/dist/scraper/session-browserbase.d.ts +13 -0
- package/dist/scraper/session-browserbase.d.ts.map +1 -1
- package/dist/scraper/session-browserbase.js +29 -0
- package/dist/scraper/session-browserbase.js.map +1 -1
- package/dist/scraper/session-ip.d.ts +69 -0
- package/dist/scraper/session-ip.d.ts.map +1 -0
- package/dist/scraper/session-ip.js +88 -0
- package/dist/scraper/session-ip.js.map +1 -0
- package/dist/scraper/session-shared.d.ts +9 -0
- package/dist/scraper/session-shared.d.ts.map +1 -1
- package/dist/scraper/session-shared.js.map +1 -1
- package/dist/scraper/submit-control.d.ts +14 -2
- package/dist/scraper/submit-control.d.ts.map +1 -1
- package/dist/scraper/submit-control.js +33 -2
- package/dist/scraper/submit-control.js.map +1 -1
- package/dist/scripts/recon-browser.d.ts.map +1 -1
- package/dist/scripts/recon-browser.js +32 -0
- package/dist/scripts/recon-browser.js.map +1 -1
- package/dist/site-plugin.d.ts +20 -0
- package/dist/site-plugin.d.ts.map +1 -1
- package/dist/testing/integration-runner.d.ts.map +1 -1
- package/dist/testing/integration-runner.js +6 -7
- package/dist/testing/integration-runner.js.map +1 -1
- package/package.json +1 -1
|
@@ -56,6 +56,7 @@ exports.writeFixtureToTempFile = writeFixtureToTempFile;
|
|
|
56
56
|
exports.attachToSurfacedInput = attachToSurfacedInput;
|
|
57
57
|
exports.surfaceAndUpload = surfaceAndUpload;
|
|
58
58
|
exports.parseSelectStep = parseSelectStep;
|
|
59
|
+
exports.parseFillStep = parseFillStep;
|
|
59
60
|
exports.parseRadioStep = parseRadioStep;
|
|
60
61
|
exports.pollEnumerate = pollEnumerate;
|
|
61
62
|
exports.waitForTransitionBody = waitForTransitionBody;
|
|
@@ -87,7 +88,10 @@ const schemas_1 = require("../lib/llm/schemas");
|
|
|
87
88
|
const logging_1 = require("../lib/logging");
|
|
88
89
|
const call_capture_1 = require("../lib/telemetry/call-capture");
|
|
89
90
|
const call_types_1 = require("../lib/telemetry/call-types");
|
|
91
|
+
const deep_locator_actuate_1 = require("../scraper/deep-locator-actuate");
|
|
90
92
|
const deep_locator_candidates_1 = require("../scraper/deep-locator-candidates");
|
|
93
|
+
const deep_locator_click_1 = require("../scraper/deep-locator-click");
|
|
94
|
+
const deep_locator_scan_1 = require("../scraper/deep-locator-scan");
|
|
91
95
|
const errors_2 = require("../scraper/errors");
|
|
92
96
|
const frame_target_1 = require("../scraper/frame-target");
|
|
93
97
|
const phantom_click_1 = require("../scraper/phantom-click");
|
|
@@ -476,7 +480,14 @@ exports.TRAILING_GRACE_WINDOW = 2;
|
|
|
476
480
|
* shape via Runtime.callFunctionOn.
|
|
477
481
|
*/
|
|
478
482
|
const DOM_SNAPSHOT_EXPR = `(() => { const b = document.body; if (!b) return { html: 0, text: "" }; const t = b.innerText || ""; return { html: (b.outerHTML || "").length, text: t.length + ":" + t.slice(0, 200) }; })()`;
|
|
479
|
-
|
|
483
|
+
/**
|
|
484
|
+
* Captures the pre/post signal triple the submit-verify cascade diffs.
|
|
485
|
+
* Accepts the optional `page` so a resolved child `FrameTarget` whose
|
|
486
|
+
* `url()` rejects (OOPIF detached by the submit it just fired) can still
|
|
487
|
+
* report the main frame's post-navigation URL instead of throwing the
|
|
488
|
+
* whole attempt out of `executeStepWithHealing`.
|
|
489
|
+
*/
|
|
490
|
+
async function snapshotPage(target, signalCounter, page) {
|
|
480
491
|
let bodyHtmlLength = 0;
|
|
481
492
|
let visibleTextSignature = "";
|
|
482
493
|
try {
|
|
@@ -495,9 +506,16 @@ async function snapshotPage(target, signalCounter) {
|
|
|
495
506
|
// Snapshot is observational; on failure, defaults to 0/"" so the verifier
|
|
496
507
|
// sees no delta. Real state-class checks already cover the verified path.
|
|
497
508
|
}
|
|
509
|
+
// A resolved child FrameTarget's url() reads location.href off the CDP
|
|
510
|
+
// frame session (frame-target.ts's childFrameTarget), which rejects (or
|
|
511
|
+
// trips its watchdog) once the OOPIF detaches — most commonly right after
|
|
512
|
+
// a submit click tears down the wizard iframe. Falling back to page.url()
|
|
513
|
+
// lets the post-submit navigation still register as a urlChanged signal
|
|
514
|
+
// instead of throwing the whole step out of the cascade.
|
|
515
|
+
const url = page ? await target.url().catch(() => page.url()) : await target.url();
|
|
498
516
|
return {
|
|
499
517
|
networkCount: signalCounter.n,
|
|
500
|
-
url
|
|
518
|
+
url,
|
|
501
519
|
bodyHtmlLength,
|
|
502
520
|
visibleTextSignature,
|
|
503
521
|
};
|
|
@@ -2687,6 +2705,40 @@ function parseSelectStep(instruction) {
|
|
|
2687
2705
|
const questionLabel = quoted.find((q) => q.trim() !== option)?.trim() ?? null;
|
|
2688
2706
|
return { option, questionLabel };
|
|
2689
2707
|
}
|
|
2708
|
+
/**
|
|
2709
|
+
* Parse a declarative TEXT-FILL flow step into the field label to target and
|
|
2710
|
+
* the value to type into it.
|
|
2711
|
+
*
|
|
2712
|
+
* Why this exists (sibling of `parseSelectStep`/`parseRadioStep`): the deep-
|
|
2713
|
+
* locator cascade's candidate ranking (`resolveDeepLocatorCandidates`'s
|
|
2714
|
+
* `scoreCandidate`) ranks by the instruction's quoted phrases, which for a
|
|
2715
|
+
* fill step is only the VALUE ("Fill in the First Name field with
|
|
2716
|
+
* 'Reginald'" quotes just `'Reginald'`) — no candidate's accessible name
|
|
2717
|
+
* (e.g. "First Name") ever matches a person's name, so every candidate ties
|
|
2718
|
+
* at score 0 and DOM order decides, clicking whatever happens to be first
|
|
2719
|
+
* rather than filling the named field. This parser recovers the FIELD LABEL
|
|
2720
|
+
* ("First Name") so the caller can match it directly against a candidate's
|
|
2721
|
+
* accessible name instead of ranking by the value.
|
|
2722
|
+
*
|
|
2723
|
+
* Recognizes the flow's conventional phrasing (confirmed against
|
|
2724
|
+
* `src/recon/fixtures/shipped-ats-flow-steps.json`):
|
|
2725
|
+
* "Fill in the First Name field with 'Reginald'",
|
|
2726
|
+
* "Fill the signature field with 'Name'",
|
|
2727
|
+
* "Fill in the Acme Non-Employee ID textbox with 'NA'".
|
|
2728
|
+
* Returns null when the step doesn't match this shape (no "fill" verb, no
|
|
2729
|
+
* field/input/textbox noun, or no quoted value) so the caller falls through
|
|
2730
|
+
* to the normal cascade unchanged.
|
|
2731
|
+
*/
|
|
2732
|
+
function parseFillStep(instruction) {
|
|
2733
|
+
const match = instruction.match(/\bfill(?:\s+in)?\s+(?:the\s+)?(.+?)\s+(?:field|input|textbox)\s+with\s+'([^']+)'/i);
|
|
2734
|
+
if (!match)
|
|
2735
|
+
return null;
|
|
2736
|
+
const fieldLabel = match[1]?.trim();
|
|
2737
|
+
const value = match[2]?.trim();
|
|
2738
|
+
if (!fieldLabel || !value)
|
|
2739
|
+
return null;
|
|
2740
|
+
return { fieldLabel, value };
|
|
2741
|
+
}
|
|
2690
2742
|
/**
|
|
2691
2743
|
* Parse a single-choice RADIO flow step into the option to click and (when
|
|
2692
2744
|
* present) the question label that scopes which radio group it targets.
|
|
@@ -2740,6 +2792,44 @@ function parseRadioStep(instruction) {
|
|
|
2740
2792
|
const questionLabel = quoted.find((q) => q.trim() !== option)?.trim() ?? null;
|
|
2741
2793
|
return { option, questionLabel };
|
|
2742
2794
|
}
|
|
2795
|
+
function resolveDeepLocatorActuation(step) {
|
|
2796
|
+
const selectParsed = parseSelectStep(step);
|
|
2797
|
+
if (selectParsed)
|
|
2798
|
+
return { kind: "select", value: selectParsed.option };
|
|
2799
|
+
const fillParsed = parseFillStep(step);
|
|
2800
|
+
if (fillParsed)
|
|
2801
|
+
return { kind: "fill", value: fillParsed.value };
|
|
2802
|
+
return { kind: "click" };
|
|
2803
|
+
}
|
|
2804
|
+
/** Whitespace-collapsed, lowercased comparison key for {@link findDeepLocatorCandidateByFieldLabel}. */
|
|
2805
|
+
function normalizeFieldLabel(text) {
|
|
2806
|
+
return text.replace(/\s+/g, " ").trim().toLowerCase();
|
|
2807
|
+
}
|
|
2808
|
+
/**
|
|
2809
|
+
* Finds the candidate whose accessible name identifies the named field —
|
|
2810
|
+
* `fieldLabel` (from {@link parseFillStep}/{@link parseSelectStep}), NOT the
|
|
2811
|
+
* value being written — among a frame's deepLocator candidates. Exact match
|
|
2812
|
+
* wins first (a bare "First Name" input against a `fieldLabel` of "First
|
|
2813
|
+
* Name"); otherwise a substring match either direction (a `fieldLabel` of
|
|
2814
|
+
* "Acme Non-Employee ID" against an accessible name of "Non-Employee ID", or
|
|
2815
|
+
* the reverse) so minor phrasing drift between the flow's field noun and the
|
|
2816
|
+
* control's own label still resolves. Returns `null` — never a guess — when
|
|
2817
|
+
* no candidate's accessible name relates to `fieldLabel` at all, so the
|
|
2818
|
+
* caller can refuse to act rather than fill/click the wrong control.
|
|
2819
|
+
*/
|
|
2820
|
+
function findDeepLocatorCandidateByFieldLabel(candidates, fieldLabel) {
|
|
2821
|
+
const normalizedLabel = normalizeFieldLabel(fieldLabel);
|
|
2822
|
+
if (!normalizedLabel)
|
|
2823
|
+
return null;
|
|
2824
|
+
const named = candidates
|
|
2825
|
+
.map((candidate) => ({ candidate, text: normalizeFieldLabel(candidate.accessibleText) }))
|
|
2826
|
+
.filter((entry) => entry.text.length > 0);
|
|
2827
|
+
const exact = named.find((entry) => entry.text === normalizedLabel);
|
|
2828
|
+
if (exact)
|
|
2829
|
+
return exact.candidate;
|
|
2830
|
+
const partial = named.find((entry) => entry.text.includes(normalizedLabel) || normalizedLabel.includes(entry.text));
|
|
2831
|
+
return partial?.candidate ?? null;
|
|
2832
|
+
}
|
|
2743
2833
|
/** Max settle-retry attempts for a primitive's DOM enumerate (see `pollEnumerate`). */
|
|
2744
2834
|
const PRIMITIVE_ENUMERATE_ATTEMPTS = 5;
|
|
2745
2835
|
/** Delay between settle-retry attempts. Total cap ≈ ATTEMPTS × this. */
|
|
@@ -4414,25 +4504,63 @@ async function probeFormValidityBeforeSubmit(params) {
|
|
|
4414
4504
|
}
|
|
4415
4505
|
}
|
|
4416
4506
|
/**
|
|
4417
|
-
*
|
|
4418
|
-
*
|
|
4419
|
-
* `
|
|
4420
|
-
*
|
|
4421
|
-
*
|
|
4422
|
-
*
|
|
4423
|
-
*
|
|
4507
|
+
* Resolves deepLocator candidates scoped to `INTERACTIVE_CANDIDATE_SELECTOR`,
|
|
4508
|
+
* widening once to the unscoped `"*"` hop when the scoped pass finds
|
|
4509
|
+
* nothing. `INTERACTIVE_CANDIDATE_SELECTOR` is what makes a dense OOPIF
|
|
4510
|
+
* form's candidate set tractable (371 nodes down to a handful — see
|
|
4511
|
+
* `deep-locator-candidates.ts`'s module docblock), but it over-narrows a
|
|
4512
|
+
* `div`/`span` tile that carries only a click handler — no `role`/
|
|
4513
|
+
* `tabindex` — so a scoped-empty result would otherwise strand an attempt
|
|
4514
|
+
* the pre-1.6.13 unscoped `"*"` hop still resolved. The widened pass only
|
|
4515
|
+
* runs when the scoped one returns zero, so the dense-form fast path
|
|
4516
|
+
* (`flow-runner.oopif-dense-form-budget.test.ts`) still costs a single
|
|
4517
|
+
* resolve. Returns the `innerSelector` actually used alongside the
|
|
4518
|
+
* candidates so a caller driving `clickDeepLocatorCandidate` re-derives the
|
|
4519
|
+
* SAME hop the candidates were ranked/indexed against.
|
|
4520
|
+
*/
|
|
4521
|
+
async function resolveDeepLocatorCandidatesWithWidening(page, frameSelector, instruction, timeoutOptions) {
|
|
4522
|
+
const scoped = await (0, deep_locator_candidates_1.resolveDeepLocatorCandidates)(page, frameSelector, deep_locator_scan_1.INTERACTIVE_CANDIDATE_SELECTOR, instruction, timeoutOptions);
|
|
4523
|
+
if (scoped.length > 0) {
|
|
4524
|
+
return { candidates: scoped, innerSelector: deep_locator_scan_1.INTERACTIVE_CANDIDATE_SELECTOR };
|
|
4525
|
+
}
|
|
4526
|
+
const widened = await (0, deep_locator_candidates_1.resolveDeepLocatorCandidates)(page, frameSelector, "*", instruction, timeoutOptions);
|
|
4527
|
+
return { candidates: widened, innerSelector: "*" };
|
|
4528
|
+
}
|
|
4529
|
+
/**
|
|
4530
|
+
* Adapts `resolveDeepLocatorCandidatesWithWidening` results into
|
|
4531
|
+
* `Action`-shaped evidence for `rephraseWithLLM`, which only knows about
|
|
4532
|
+
* Stagehand's `Action` type. Never throws: `resolveDeepLocatorCandidates`
|
|
4533
|
+
* itself degrades to `[]` on a resolver failure, so this only feeds the
|
|
4534
|
+
* rephrase prompt richer evidence when available — a frame-scoped step
|
|
4535
|
+
* whose observe AND deepLocator (scoped, then widened) both come back
|
|
4536
|
+
* empty just gets the same `[]` evidence it would have gotten before this
|
|
4537
|
+
* fix.
|
|
4538
|
+
*
|
|
4539
|
+
* `instruction` is forwarded so the evidence list is ranked by relevance to
|
|
4540
|
+
* the step, same as the act path — an unranked `[]`-then-DOM-order list
|
|
4541
|
+
* would feed the rephrase LLM its worst evidence first instead of its best.
|
|
4542
|
+
* It's also what {@link resolveDeepLocatorActuation} reads to pick the
|
|
4543
|
+
* evidence's `method`/`arguments` — a rephrase evaluating a "Fill in the
|
|
4544
|
+
* First Name field with 'Reginald'" step should see fill/"Reginald"
|
|
4545
|
+
* evidence, not a hard-coded click, now that the cascade itself can
|
|
4546
|
+
* actuate fill/select. Omitted (the unfocused-observe evidence list) falls
|
|
4547
|
+
* back to `click` with no value, same as before this method inference
|
|
4548
|
+
* existed.
|
|
4424
4549
|
*
|
|
4425
|
-
*
|
|
4426
|
-
*
|
|
4427
|
-
*
|
|
4428
|
-
* worst evidence first instead of its best.
|
|
4550
|
+
* Takes the caller's already-resolved `frameTarget` so
|
|
4551
|
+
* `resolveDeepLocatorCandidates` uses the batched frame-scoped evaluate
|
|
4552
|
+
* instead of re-resolving it internally.
|
|
4429
4553
|
*/
|
|
4430
|
-
async function deepLocatorCandidatesAsActions(page,
|
|
4431
|
-
const candidates = await (
|
|
4554
|
+
async function deepLocatorCandidatesAsActions(page, frameTarget, instruction) {
|
|
4555
|
+
const { candidates } = await resolveDeepLocatorCandidatesWithWidening(page, frameTarget.frameSelector, instruction, { frameTarget });
|
|
4556
|
+
const actuation = instruction
|
|
4557
|
+
? resolveDeepLocatorActuation(instruction)
|
|
4558
|
+
: { kind: "click" };
|
|
4432
4559
|
return candidates.map((c) => ({
|
|
4433
4560
|
selector: c.selector,
|
|
4434
4561
|
description: c.accessibleText || "(no accessible text)",
|
|
4435
|
-
method: "click",
|
|
4562
|
+
method: actuation.kind === "select" ? "selectOption" : actuation.kind === "fill" ? "fill" : "click",
|
|
4563
|
+
...(actuation.kind === "click" ? {} : { arguments: [actuation.value] }),
|
|
4436
4564
|
}));
|
|
4437
4565
|
}
|
|
4438
4566
|
/**
|
|
@@ -4479,7 +4607,16 @@ async function probeStepBeforeAttempts(params) {
|
|
|
4479
4607
|
? await reresolveFrameTarget()
|
|
4480
4608
|
: frameTarget;
|
|
4481
4609
|
if (effectiveFrameTarget?.frame) {
|
|
4482
|
-
|
|
4610
|
+
// Pass the already-resolved `effectiveFrameTarget` so the batched
|
|
4611
|
+
// evaluate reuses it instead of `resolveDeepLocatorCandidates`
|
|
4612
|
+
// re-resolving via its own internal `resolveFrameTarget` fallback —
|
|
4613
|
+
// same "reuse the ambient target" rule as every other frame-scoped
|
|
4614
|
+
// call in this function. Innerselector stays "*": this probe only
|
|
4615
|
+
// answers "does this frame have any content", and batching already
|
|
4616
|
+
// makes that reachability check cheap (one evaluate over every
|
|
4617
|
+
// node) — scoping to interactive elements would false-negative a
|
|
4618
|
+
// frame with rendered content but no controls yet.
|
|
4619
|
+
const deepLocatorCandidates = await (0, deep_locator_candidates_1.resolveDeepLocatorCandidates)(page, effectiveFrameTarget.frameSelector, "*", undefined, { frameTarget: effectiveFrameTarget });
|
|
4483
4620
|
if (deepLocatorCandidates.length > 0) {
|
|
4484
4621
|
logger.info(`${formatStepPrefix(stepIndex, totalSteps)}: observe found 0 candidates (focused and unfocused) but deepLocator found ${deepLocatorCandidates.length} — treating as present (let cascade resolve)`);
|
|
4485
4622
|
return "present";
|
|
@@ -4744,7 +4881,7 @@ async function executeStepWithHealing(params) {
|
|
|
4744
4881
|
// replan budget on every frame-scoped probe-absent failure.
|
|
4745
4882
|
await reresolveFrameTargetIfLost();
|
|
4746
4883
|
const unfocusedObserve = probeAbsentObservedUnfocused.length === 0 && frameTarget?.frame
|
|
4747
|
-
? await deepLocatorCandidatesAsActions(page, frameTarget
|
|
4884
|
+
? await deepLocatorCandidatesAsActions(page, frameTarget)
|
|
4748
4885
|
: probeAbsentObservedUnfocused;
|
|
4749
4886
|
const dumpPath = onStepFailure?.({
|
|
4750
4887
|
stepIndex,
|
|
@@ -4884,7 +5021,7 @@ async function executeStepWithHealing(params) {
|
|
|
4884
5021
|
if (attempt > 1) {
|
|
4885
5022
|
await page.waitForTimeout(attempt * ATTEMPT_BACKOFF_MS);
|
|
4886
5023
|
}
|
|
4887
|
-
const pre = await snapshotPage(frameTarget ?? (0, frame_target_1.mainFrameTarget)(page), signalCounter);
|
|
5024
|
+
const pre = await snapshotPage(frameTarget ?? (0, frame_target_1.mainFrameTarget)(page), signalCounter, page);
|
|
4888
5025
|
// Snapshot the meta-tail length so the final-step pattern gate can scope
|
|
4889
5026
|
// its URL scan to captures added DURING this attempt (not historical
|
|
4890
5027
|
// tail from earlier steps).
|
|
@@ -5017,7 +5154,7 @@ async function executeStepWithHealing(params) {
|
|
|
5017
5154
|
// burn the step budget probing all of them.
|
|
5018
5155
|
const runnerUp = ranked[1];
|
|
5019
5156
|
if (runnerUp) {
|
|
5020
|
-
const midPost = await snapshotPage(frameTarget ?? (0, frame_target_1.mainFrameTarget)(page), signalCounter);
|
|
5157
|
+
const midPost = await snapshotPage(frameTarget ?? (0, frame_target_1.mainFrameTarget)(page), signalCounter, page);
|
|
5021
5158
|
const topVerdict = (0, phantom_click_1.classifyPhantomClick)({
|
|
5022
5159
|
actResultSuccess: true,
|
|
5023
5160
|
pre,
|
|
@@ -5079,45 +5216,206 @@ async function executeStepWithHealing(params) {
|
|
|
5079
5216
|
// exclusion is applied here by filtering resolved (already-ranked)
|
|
5080
5217
|
// candidates against triedSelectors instead — otherwise attempt 4
|
|
5081
5218
|
// would re-pick the same failed element and burn the attempt.
|
|
5219
|
+
// `resolveDeepLocatorCandidatesWithWidening` widens the hop to `"*"`
|
|
5220
|
+
// once when the interactive-scoped pass finds nothing — a
|
|
5221
|
+
// `div`/`span` tile with only a click handler (no role/tabindex)
|
|
5222
|
+
// matches nothing scoped, so the widened pass is what still
|
|
5223
|
+
// surfaces it — and reports which innerSelector actually resolved
|
|
5224
|
+
// so the click below re-derives the same hop, not a stale one.
|
|
5082
5225
|
await reresolveFrameTargetIfLost();
|
|
5083
|
-
const
|
|
5084
|
-
?
|
|
5085
|
-
:
|
|
5226
|
+
const deepLocatorResolution = candidates.length === 0 && frameTarget?.frame
|
|
5227
|
+
? await resolveDeepLocatorCandidatesWithWidening(page, frameTarget.frameSelector, step, { frameTarget })
|
|
5228
|
+
: null;
|
|
5229
|
+
const deepLocatorInnerSelector = deepLocatorResolution?.innerSelector ?? deep_locator_scan_1.INTERACTIVE_CANDIDATE_SELECTOR;
|
|
5230
|
+
const deepLocatorCandidates = (deepLocatorResolution?.candidates ?? []).filter((c) => !triedSelectors.includes(c.selector));
|
|
5086
5231
|
if (candidates.length === 0 && deepLocatorCandidates.length > 0) {
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
|
|
5095
|
-
|
|
5096
|
-
|
|
5232
|
+
// A fill/select step must actuate the NAMED FIELD through the
|
|
5233
|
+
// fill/select seam, never the click-only walk below: candidate
|
|
5234
|
+
// ranking scores by the instruction's quoted VALUE (see
|
|
5235
|
+
// parseFillStep's docblock) — for "Fill in the First Name field
|
|
5236
|
+
// with 'Reginald'" no control's accessible name is ever 'Reginald',
|
|
5237
|
+
// so every candidate ties at score 0 and the click walk would fire
|
|
5238
|
+
// on whatever sits first in DOM order (the bug report's wizard
|
|
5239
|
+
// 'Close' mis-click). Detect that shape first and route
|
|
5240
|
+
// deterministically to the candidate matching the field's own
|
|
5241
|
+
// label, refusing to click when no candidate names that field.
|
|
5242
|
+
const fillStep = parseFillStep(step);
|
|
5243
|
+
const selectStep = fillStep ? null : parseSelectStep(step);
|
|
5244
|
+
const fieldTarget = fillStep
|
|
5245
|
+
? { kind: "fill", fieldLabel: fillStep.fieldLabel, value: fillStep.value }
|
|
5246
|
+
: selectStep?.questionLabel
|
|
5247
|
+
? { kind: "select", fieldLabel: selectStep.questionLabel, value: selectStep.option }
|
|
5248
|
+
: null;
|
|
5249
|
+
if (fieldTarget) {
|
|
5250
|
+
const matched = findDeepLocatorCandidateByFieldLabel(deepLocatorCandidates, fieldTarget.fieldLabel);
|
|
5251
|
+
if (!matched) {
|
|
5252
|
+
const failureMessage = `deepLocator: no candidate matched field "${fieldTarget.fieldLabel}" (refusing to click an unrelated control)`;
|
|
5253
|
+
record.actResultSuccess = false;
|
|
5254
|
+
record.errorMessage = failureMessage;
|
|
5097
5255
|
attempts.push(record);
|
|
5098
|
-
failureReasons.push(
|
|
5099
|
-
logger.info(`${formatStepPrefix(stepIndex, totalSteps)} attempt ${attempt}: ${
|
|
5256
|
+
failureReasons.push(failureMessage);
|
|
5257
|
+
logger.info(`${formatStepPrefix(stepIndex, totalSteps)} attempt ${attempt}: ${failureMessage}`);
|
|
5100
5258
|
continue;
|
|
5101
5259
|
}
|
|
5102
|
-
triedSelectors.push(
|
|
5103
|
-
record.triedSelectors = [
|
|
5104
|
-
|
|
5105
|
-
await (0,
|
|
5260
|
+
triedSelectors.push(matched.selector);
|
|
5261
|
+
record.triedSelectors = [matched.selector];
|
|
5262
|
+
const actuated = fieldTarget.kind === "fill"
|
|
5263
|
+
? await (0, deep_locator_actuate_1.fillDeepLocatorCandidate)(page, frameTarget?.frameSelector, deepLocatorInnerSelector, matched.index, fieldTarget.value, { frameTarget })
|
|
5264
|
+
: await (0, deep_locator_actuate_1.selectDeepLocatorCandidateOption)(page, frameTarget?.frameSelector, deepLocatorInnerSelector, matched.index, fieldTarget.value, { frameTarget });
|
|
5265
|
+
if (actuated) {
|
|
5266
|
+
record.instruction = `deepLocator: ${matched.accessibleText || "(no accessible text)"}`;
|
|
5106
5267
|
record.actResultSuccess = true;
|
|
5107
|
-
record.actResultDescription = `deepLocator
|
|
5108
|
-
//
|
|
5109
|
-
//
|
|
5110
|
-
//
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5268
|
+
record.actResultDescription = `deepLocator ${fieldTarget.kind === "fill" ? "filled" : "selected"} "${matched.accessibleText || matched.selector}" with "${fieldTarget.value}"`;
|
|
5269
|
+
// verifyDomEffect can't resolve a `deeplocator=` selector (see
|
|
5270
|
+
// deep-locator-actuate.ts's module docblock: target.locator()
|
|
5271
|
+
// has no meaning for cross-origin OOPIF hop notation) — the
|
|
5272
|
+
// write + read-back fillDeepLocatorCandidate/
|
|
5273
|
+
// selectDeepLocatorCandidateOption already performed IS the
|
|
5274
|
+
// verification signal, so record it directly instead of
|
|
5275
|
+
// synthesizing a resolvedAction that a verifier can only ever
|
|
5276
|
+
// score false for.
|
|
5277
|
+
record.verifiedBy = "dom";
|
|
5278
|
+
attempts.push(record);
|
|
5279
|
+
logger.info(`${formatStepPrefix(stepIndex, totalSteps)} attempt ${attempt}: ${record.actResultDescription}`);
|
|
5280
|
+
trajectory?.push({ stepIndex, verifiedBy: "dom" });
|
|
5281
|
+
return "completed";
|
|
5116
5282
|
}
|
|
5117
|
-
|
|
5283
|
+
const failureMessage = `deepLocator: ${fieldTarget.kind} on "${matched.accessibleText || matched.selector}" did not verify (read-back mismatch or rejected write)`;
|
|
5284
|
+
record.actResultSuccess = false;
|
|
5285
|
+
record.errorMessage = failureMessage;
|
|
5286
|
+
attempts.push(record);
|
|
5287
|
+
failureReasons.push(failureMessage);
|
|
5288
|
+
logger.info(`${formatStepPrefix(stepIndex, totalSteps)} attempt ${attempt}: ${failureMessage}`);
|
|
5289
|
+
continue;
|
|
5290
|
+
}
|
|
5291
|
+
// A select step whose question is phrased un-quoted (parseSelectStep
|
|
5292
|
+
// returns `questionLabel: null`) has no fieldLabel to route through
|
|
5293
|
+
// findDeepLocatorCandidateByFieldLabel above, so ranking below falls
|
|
5294
|
+
// back to the OPTION value alone. Scoring the full interactive-scoped
|
|
5295
|
+
// `deepLocatorCandidates` here (buttons, inputs, links alongside any
|
|
5296
|
+
// <select>) would refuse on virtually every dense form: an unrelated
|
|
5297
|
+
// 'First Name' input or 'Submit Application' button never mentions
|
|
5298
|
+
// the quoted option any more than the genuine <select> target does,
|
|
5299
|
+
// so every one of them ties at score 0 right alongside it — not just
|
|
5300
|
+
// the two genuinely ambiguous <select>s the guard exists to catch.
|
|
5301
|
+
// A fresh resolve scoped to "select" narrows the tie check to only
|
|
5302
|
+
// the candidates that could actually satisfy a select-write (a
|
|
5303
|
+
// non-<select> candidate already reports "not-actionable" and is
|
|
5304
|
+
// skipped by the walk below regardless — see
|
|
5305
|
+
// buildSelectFrameCandidateExpr's `el.options || []` lookup), so
|
|
5306
|
+
// this only refuses when two (or more) real <select>s tie for
|
|
5307
|
+
// relevance, e.g. two 'Yes'/'No' controls whose own accessible names
|
|
5308
|
+
// never mention the option text. A unique top-ranked <select>
|
|
5309
|
+
// (including the common single-<select>-in-frame case, where a tie
|
|
5310
|
+
// is impossible) still reaches the walk.
|
|
5311
|
+
if (selectStep && !selectStep.questionLabel) {
|
|
5312
|
+
const selectCandidates = await (0, deep_locator_candidates_1.resolveDeepLocatorCandidates)(page, frameTarget?.frameSelector, "select", null, { frameTarget });
|
|
5313
|
+
const optionPhrases = (0, deep_locator_candidates_1.extractTaggedPhrases)(step);
|
|
5314
|
+
const optionScores = selectCandidates.map((candidate) => (0, deep_locator_candidates_1.scoreCandidate)(candidate.accessibleText, optionPhrases));
|
|
5315
|
+
const topScore = optionScores.length > 0 ? Math.max(...optionScores) : 0;
|
|
5316
|
+
const tiedForTop = optionScores.filter((score) => score === topScore).length;
|
|
5317
|
+
if (tiedForTop > 1) {
|
|
5318
|
+
const failureMessage = `deepLocator: select step's question is un-quoted and ${tiedForTop} candidates tie for relevance to "${selectStep.option}" (refusing to guess which control to select)`;
|
|
5118
5319
|
record.actResultSuccess = false;
|
|
5119
|
-
record.errorMessage =
|
|
5320
|
+
record.errorMessage = failureMessage;
|
|
5321
|
+
attempts.push(record);
|
|
5322
|
+
failureReasons.push(failureMessage);
|
|
5323
|
+
logger.info(`${formatStepPrefix(stepIndex, totalSteps)} attempt ${attempt}: ${failureMessage}`);
|
|
5324
|
+
continue;
|
|
5325
|
+
}
|
|
5326
|
+
}
|
|
5327
|
+
// Actionable-candidate walk: a top pick that rejects with the CDP
|
|
5328
|
+
// `-32000 Node does not have a layout object` error (an unrendered
|
|
5329
|
+
// node) or is refused by the wizard-exit deny-list costs only that
|
|
5330
|
+
// one candidate, not the whole attempt — the next ranked candidate
|
|
5331
|
+
// is tried instead. `attemptTriedSelectors` mirrors the walk's own
|
|
5332
|
+
// click attempts as they happen (not just on a successful return)
|
|
5333
|
+
// so a click that throws a REAL error (e.g. a wedged
|
|
5334
|
+
// `WatchdogTimeoutError`) still feeds attempt 4's exclusion filter.
|
|
5335
|
+
const attemptTriedSelectors = [];
|
|
5336
|
+
const denyCandidate = (candidate) => {
|
|
5337
|
+
const denied = isWizardExitAction(candidate.accessibleText, wizardExitButtonLabels);
|
|
5338
|
+
if (denied) {
|
|
5339
|
+
logger.info(`${formatStepPrefix(stepIndex, totalSteps)} attempt ${attempt}: refused wizard-exit control: "${candidate.accessibleText.slice(0, 60)}"`);
|
|
5340
|
+
}
|
|
5341
|
+
return denied;
|
|
5342
|
+
};
|
|
5343
|
+
// Intent discrimination: observe()'s Stagehand-resolved action
|
|
5344
|
+
// carries its own method + fill/select arguments (see attempt 1's
|
|
5345
|
+
// `target.method === "fill"` handling above); the deepLocator
|
|
5346
|
+
// fallback has no such resolved action, so the step prose is the
|
|
5347
|
+
// only place the fill/select value can come from. Derived once per
|
|
5348
|
+
// attempt (not per candidate) — every candidate in this walk is a
|
|
5349
|
+
// guess at the SAME step's target, so they all actuate the same way.
|
|
5350
|
+
const actuation = resolveDeepLocatorActuation(step);
|
|
5351
|
+
const cascadeResult = await (0, deep_locator_click_1.clickFirstActionableCandidate)(deepLocatorCandidates, async (candidate) => {
|
|
5352
|
+
attemptTriedSelectors.push(candidate.selector);
|
|
5353
|
+
if (actuation.kind === "select") {
|
|
5354
|
+
const verified = await (0, deep_locator_actuate_1.selectDeepLocatorCandidateOption)(page, frameTarget?.frameSelector, deepLocatorInnerSelector, candidate.index, actuation.value, { frameTarget });
|
|
5355
|
+
// selectDeepLocatorCandidateOption never throws on an ordinary
|
|
5356
|
+
// failed write (see deep-locator-actuate.ts's writeAndVerify) —
|
|
5357
|
+
// it resolves `false` for both a rejected selectOption() and a
|
|
5358
|
+
// read-back mismatch. clickFirstActionableCandidate infers
|
|
5359
|
+
// success from "didn't throw", so a `false` here must become a
|
|
5360
|
+
// throw or the walk would wrongly report this candidate as
|
|
5361
|
+
// actuated.
|
|
5362
|
+
if (!verified)
|
|
5363
|
+
throw new Error("-32000 Node does not have a layout object");
|
|
5120
5364
|
}
|
|
5365
|
+
else if (actuation.kind === "fill") {
|
|
5366
|
+
const verified = await (0, deep_locator_actuate_1.fillDeepLocatorCandidate)(page, frameTarget?.frameSelector, deepLocatorInnerSelector, candidate.index, actuation.value, { frameTarget });
|
|
5367
|
+
if (!verified)
|
|
5368
|
+
throw new Error("-32000 Node does not have a layout object");
|
|
5369
|
+
}
|
|
5370
|
+
else {
|
|
5371
|
+
await (0, deep_locator_candidates_1.clickDeepLocatorCandidate)(page, frameTarget?.frameSelector, deepLocatorInnerSelector, candidate.index, { frameTarget });
|
|
5372
|
+
}
|
|
5373
|
+
}, { denyCandidate })
|
|
5374
|
+
.then((outcome) => ({ outcome, error: null }))
|
|
5375
|
+
.catch((error) => ({ outcome: null, error }));
|
|
5376
|
+
triedSelectors.push(...attemptTriedSelectors);
|
|
5377
|
+
record.triedSelectors = [...attemptTriedSelectors];
|
|
5378
|
+
if (cascadeResult.outcome?.clicked && cascadeResult.outcome.candidate) {
|
|
5379
|
+
const acted = cascadeResult.outcome.candidate;
|
|
5380
|
+
const verb = actuation.kind === "select"
|
|
5381
|
+
? "selected"
|
|
5382
|
+
: actuation.kind === "fill"
|
|
5383
|
+
? "filled"
|
|
5384
|
+
: "clicked";
|
|
5385
|
+
record.instruction = `deepLocator: ${acted.accessibleText || "(no accessible text)"}`;
|
|
5386
|
+
record.actResultSuccess = true;
|
|
5387
|
+
record.actResultDescription = `deepLocator ${verb} "${acted.accessibleText || acted.selector}"`;
|
|
5388
|
+
// Synthesize an action matching the actuation kind so downstream
|
|
5389
|
+
// verification (network/url/dom — STATE_CLASS_METHODS treats
|
|
5390
|
+
// fill/selectOption as DOM-verifiable, not click) treats this
|
|
5391
|
+
// exactly like any other resolved action — same idiom as
|
|
5392
|
+
// deep-submit-locator/structured-click.
|
|
5393
|
+
resolvedAction =
|
|
5394
|
+
actuation.kind === "click"
|
|
5395
|
+
? {
|
|
5396
|
+
selector: acted.selector,
|
|
5397
|
+
description: record.actResultDescription,
|
|
5398
|
+
method: "click",
|
|
5399
|
+
}
|
|
5400
|
+
: {
|
|
5401
|
+
selector: acted.selector,
|
|
5402
|
+
description: record.actResultDescription,
|
|
5403
|
+
method: actuation.kind === "select" ? "selectOption" : "fill",
|
|
5404
|
+
arguments: [actuation.value],
|
|
5405
|
+
};
|
|
5406
|
+
}
|
|
5407
|
+
else {
|
|
5408
|
+
const failureMessage = cascadeResult.error
|
|
5409
|
+
? `deepLocator: ${actuation.kind} threw ${(0, errors_1.toErrorMessage)(cascadeResult.error)}`
|
|
5410
|
+
: attemptTriedSelectors.length > 0
|
|
5411
|
+
? `deepLocator: no actionable candidate (${attemptTriedSelectors.length} not-actionable)`
|
|
5412
|
+
: "deepLocator: no actionable candidate (every candidate refused by the wizard-exit deny-list)";
|
|
5413
|
+
record.actResultSuccess = false;
|
|
5414
|
+
record.errorMessage = failureMessage;
|
|
5415
|
+
attempts.push(record);
|
|
5416
|
+
failureReasons.push(failureMessage);
|
|
5417
|
+
logger.info(`${formatStepPrefix(stepIndex, totalSteps)} attempt ${attempt}: ${failureMessage}`);
|
|
5418
|
+
continue;
|
|
5121
5419
|
}
|
|
5122
5420
|
}
|
|
5123
5421
|
else if (candidates.length === 0) {
|
|
@@ -5395,7 +5693,7 @@ async function executeStepWithHealing(params) {
|
|
|
5395
5693
|
// prompt, so a resolver error/empty result is fine to swallow.
|
|
5396
5694
|
await reresolveFrameTargetIfLost();
|
|
5397
5695
|
const candidates = observedCandidates.length === 0 && frameTarget?.frame
|
|
5398
|
-
? await deepLocatorCandidatesAsActions(page, frameTarget
|
|
5696
|
+
? await deepLocatorCandidatesAsActions(page, frameTarget, step)
|
|
5399
5697
|
: observedCandidates;
|
|
5400
5698
|
// Fetch live-page evidence so the rephrase prompt can reason about
|
|
5401
5699
|
// form state, not just observe candidates. Mirrors the same
|
|
@@ -5413,7 +5711,7 @@ async function executeStepWithHealing(params) {
|
|
|
5413
5711
|
// alongside the failed step, not in the top document.
|
|
5414
5712
|
const observedUnfocused = await (0, stagehand_guard_1.guardedObserve)(stagehand, undefined, { timeout: exports.STEP_WATCHDOG_MS }, captureFn, frameTarget).catch(() => []);
|
|
5415
5713
|
const unfocused = observedUnfocused.length === 0 && frameTarget?.frame
|
|
5416
|
-
? await deepLocatorCandidatesAsActions(page, frameTarget
|
|
5714
|
+
? await deepLocatorCandidatesAsActions(page, frameTarget)
|
|
5417
5715
|
: observedUnfocused;
|
|
5418
5716
|
const submitFailureList = extractSubmitFailureEvidence(recentCaptures, ownBackendHostnames);
|
|
5419
5717
|
const gaEventList = extractGaEventEvidence(recentCaptures);
|
|
@@ -5466,7 +5764,7 @@ async function executeStepWithHealing(params) {
|
|
|
5466
5764
|
continue;
|
|
5467
5765
|
}
|
|
5468
5766
|
await page.waitForTimeout(STEP_PAUSE_MS);
|
|
5469
|
-
const post = await snapshotPage(frameTarget ?? (0, frame_target_1.mainFrameTarget)(page), signalCounter);
|
|
5767
|
+
const post = await snapshotPage(frameTarget ?? (0, frame_target_1.mainFrameTarget)(page), signalCounter, page);
|
|
5470
5768
|
record.post = post;
|
|
5471
5769
|
if (resolvedAction) {
|
|
5472
5770
|
record.resolvedMethod = resolvedAction.method ?? null;
|
|
@@ -5736,7 +6034,7 @@ async function executeStepWithHealing(params) {
|
|
|
5736
6034
|
probeResult.checked === true &&
|
|
5737
6035
|
!ancestorStillInvalid;
|
|
5738
6036
|
await page.waitForTimeout(STEP_PAUSE_MS);
|
|
5739
|
-
const retryPost = await snapshotPage(frameTarget ?? (0, frame_target_1.mainFrameTarget)(page), signalCounter);
|
|
6037
|
+
const retryPost = await snapshotPage(frameTarget ?? (0, frame_target_1.mainFrameTarget)(page), signalCounter, page);
|
|
5740
6038
|
const retryNetworkFired = retryPost.networkCount > pre.networkCount;
|
|
5741
6039
|
const retryUrlChanged = retryPost.url !== pre.url;
|
|
5742
6040
|
const retryHtmlDelta = retryPost.bodyHtmlLength - pre.bodyHtmlLength;
|
|
@@ -6035,7 +6333,7 @@ async function executeStepWithHealing(params) {
|
|
|
6035
6333
|
// replan budget on every frame-scoped cascade-exhaust failure.
|
|
6036
6334
|
await reresolveFrameTargetIfLost();
|
|
6037
6335
|
const finalObserve = cascadeExhaustObservedFinal.length === 0 && frameTarget?.frame
|
|
6038
|
-
? await deepLocatorCandidatesAsActions(page, frameTarget
|
|
6336
|
+
? await deepLocatorCandidatesAsActions(page, frameTarget, step)
|
|
6039
6337
|
: cascadeExhaustObservedFinal;
|
|
6040
6338
|
const { pageTitle, pageUrl } = await resolveDumpPageIdentity(page, frameTarget);
|
|
6041
6339
|
// Discriminator data for "Stagehand sees nothing" failures: capture the raw
|
|
@@ -6047,7 +6345,7 @@ async function executeStepWithHealing(params) {
|
|
|
6047
6345
|
const bodyOuterHtml = typeof bodyOuterHtmlRaw === "string" ? bodyOuterHtmlRaw.slice(0, 100_000) : null;
|
|
6048
6346
|
const cascadeExhaustObservedUnfocused = await (0, stagehand_guard_1.guardedObserve)(stagehand, undefined, { timeout: exports.STEP_WATCHDOG_MS }, captureFn, frameTarget).catch(() => []);
|
|
6049
6347
|
const unfocusedObserve = cascadeExhaustObservedUnfocused.length === 0 && frameTarget?.frame
|
|
6050
|
-
? await deepLocatorCandidatesAsActions(page, frameTarget
|
|
6348
|
+
? await deepLocatorCandidatesAsActions(page, frameTarget)
|
|
6051
6349
|
: cascadeExhaustObservedUnfocused;
|
|
6052
6350
|
const dumpPath = onStepFailure?.({
|
|
6053
6351
|
stepIndex,
|