@enricai/barnacle 1.9.12 → 1.10.1
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/deep-locator-candidates.d.ts +42 -12
- package/dist/scraper/deep-locator-candidates.d.ts.map +1 -1
- package/dist/scraper/deep-locator-candidates.js +48 -21
- package/dist/scraper/deep-locator-candidates.js.map +1 -1
- package/dist/scraper/deep-locator-scan.d.ts +23 -13
- package/dist/scraper/deep-locator-scan.d.ts.map +1 -1
- package/dist/scraper/deep-locator-scan.js +23 -13
- package/dist/scraper/deep-locator-scan.js.map +1 -1
- package/dist/scraper/flow-runner.d.ts +7 -7
- package/dist/scraper/flow-runner.d.ts.map +1 -1
- package/dist/scraper/flow-runner.js +238 -123
- package/dist/scraper/flow-runner.js.map +1 -1
- package/package.json +1 -1
|
@@ -5572,6 +5572,114 @@ async function executeStepWithHealing(params) {
|
|
|
5572
5572
|
// observable effect (see classifyPhantomClick). Reroutes attempt 2 to
|
|
5573
5573
|
// deep-submit-locator instead of observe-act, since re-resolving via the
|
|
5574
5574
|
// same light-DOM view cannot reach a target the resolver can't see.
|
|
5575
|
+
// Shared deepLocator actionable-candidate walk, invoked by BOTH the
|
|
5576
|
+
// observe-blind attempt-2/4 branch and the phantom-driven `trusted-click-retry`
|
|
5577
|
+
// branch. Walks ranked `candidates`, skipping wizard-exit and not-actionable
|
|
5578
|
+
// ones, actuating the first that sticks (`clickFirstActionableCandidate`), then
|
|
5579
|
+
// synthesizes a `resolvedAction` the standard verifier consumes. Returns
|
|
5580
|
+
// `resolvedAction` on success, or `null` on failure (the caller `continue`s).
|
|
5581
|
+
// `preferTrustedClick` forwards to `clickDeepLocatorCandidate` so the retry
|
|
5582
|
+
// path activates via the trusted CDP click; `labelPrefix` distinguishes the
|
|
5583
|
+
// two callers in the audit record. On failure the running `triedSelectors`
|
|
5584
|
+
// (which already holds prior attempts' resolved xpath) is written to
|
|
5585
|
+
// `record.triedSelectors`, not just this walk's own — so a walk that resolved
|
|
5586
|
+
// only deny-listed candidates still exposes a prior xpath to
|
|
5587
|
+
// `shouldSkipTechnique`'s `anyXpathResolved` gate, keeping `structured-click`
|
|
5588
|
+
// reachable at attempt 3.
|
|
5589
|
+
const runDeepLocatorClickWalk = async (args) => {
|
|
5590
|
+
const { candidates, innerSelector, preferTrustedClick, labelPrefix, record, attempt, pre } = args;
|
|
5591
|
+
const attemptTriedSelectors = [];
|
|
5592
|
+
const denyCandidate = (candidate) => {
|
|
5593
|
+
const denied = isWizardExitAction(candidate.accessibleText, wizardExitButtonLabels);
|
|
5594
|
+
if (denied) {
|
|
5595
|
+
logger.info(`${formatStepPrefix(stepIndex, totalSteps)} attempt ${attempt}: refused wizard-exit control: "${candidate.accessibleText.slice(0, 60)}"`);
|
|
5596
|
+
}
|
|
5597
|
+
return denied;
|
|
5598
|
+
};
|
|
5599
|
+
const actuation = resolveDeepLocatorActuation(step);
|
|
5600
|
+
const isSelectionIntentClick = actuation.kind === "click" && parseSelectStep(step) !== null;
|
|
5601
|
+
const baselineSelectionCount = isSelectionIntentClick
|
|
5602
|
+
? selectionCountFromSignature(pre.visibleTextSignature)
|
|
5603
|
+
: null;
|
|
5604
|
+
const readSelectionCount = isSelectionIntentClick
|
|
5605
|
+
? async () => {
|
|
5606
|
+
try {
|
|
5607
|
+
const snap = await snapshotPage(frameTarget ?? (0, frame_target_1.mainFrameTarget)(page), signalCounter, page);
|
|
5608
|
+
return selectionCountFromSignature(snap.visibleTextSignature);
|
|
5609
|
+
}
|
|
5610
|
+
catch {
|
|
5611
|
+
return null;
|
|
5612
|
+
}
|
|
5613
|
+
}
|
|
5614
|
+
: undefined;
|
|
5615
|
+
const cascadeResult = await (0, deep_locator_click_1.clickFirstActionableCandidate)(candidates, async (candidate) => {
|
|
5616
|
+
attemptTriedSelectors.push(candidate.selector);
|
|
5617
|
+
if (actuation.kind === "select") {
|
|
5618
|
+
const verified = await (0, deep_locator_actuate_1.selectDeepLocatorCandidateOption)(page, frameTarget?.frameSelector, innerSelector, candidate.index, actuation.value, { frameTarget });
|
|
5619
|
+
// selectDeepLocatorCandidateOption never throws on an ordinary failed
|
|
5620
|
+
// write (see deep-locator-actuate.ts's writeAndVerify) — it resolves
|
|
5621
|
+
// `false` for both a rejected selectOption() and a read-back mismatch.
|
|
5622
|
+
// clickFirstActionableCandidate infers success from "didn't throw", so
|
|
5623
|
+
// a `false` here must become a throw or the walk would wrongly report
|
|
5624
|
+
// this candidate as actuated.
|
|
5625
|
+
if (!verified)
|
|
5626
|
+
throw new Error("-32000 Node does not have a layout object");
|
|
5627
|
+
}
|
|
5628
|
+
else if (actuation.kind === "fill") {
|
|
5629
|
+
const verified = await (0, deep_locator_actuate_1.fillDeepLocatorCandidate)(page, frameTarget?.frameSelector, innerSelector, candidate.index, actuation.value, { frameTarget });
|
|
5630
|
+
if (!verified)
|
|
5631
|
+
throw new Error("-32000 Node does not have a layout object");
|
|
5632
|
+
}
|
|
5633
|
+
else {
|
|
5634
|
+
await (0, deep_locator_candidates_1.clickDeepLocatorCandidate)(page, frameTarget?.frameSelector, innerSelector, candidate.index, {
|
|
5635
|
+
frameTarget,
|
|
5636
|
+
preferTrustedClick,
|
|
5637
|
+
});
|
|
5638
|
+
}
|
|
5639
|
+
}, { denyCandidate, readSelectionCount, baselineSelectionCount })
|
|
5640
|
+
.then((outcome) => ({ outcome, error: null }))
|
|
5641
|
+
.catch((error) => ({ outcome: null, error }));
|
|
5642
|
+
triedSelectors.push(...attemptTriedSelectors);
|
|
5643
|
+
if (cascadeResult.outcome?.clicked && cascadeResult.outcome.candidate) {
|
|
5644
|
+
const acted = cascadeResult.outcome.candidate;
|
|
5645
|
+
record.triedSelectors = [...attemptTriedSelectors];
|
|
5646
|
+
const verb = actuation.kind === "select" ? "selected" : actuation.kind === "fill" ? "filled" : "clicked";
|
|
5647
|
+
record.instruction = `${labelPrefix}: ${acted.accessibleText || "(no accessible text)"}`;
|
|
5648
|
+
record.actResultSuccess = true;
|
|
5649
|
+
record.actResultDescription = `${labelPrefix} ${verb} "${acted.accessibleText || acted.selector}"`;
|
|
5650
|
+
// Synthesize an action matching the actuation kind so downstream
|
|
5651
|
+
// verification (network/url/dom — STATE_CLASS_METHODS treats
|
|
5652
|
+
// fill/selectOption as DOM-verifiable, not click) treats this exactly like
|
|
5653
|
+
// any other resolved action — same idiom as deep-submit-locator/structured-click.
|
|
5654
|
+
const resolvedAction = actuation.kind === "click"
|
|
5655
|
+
? { selector: acted.selector, description: record.actResultDescription, method: "click" }
|
|
5656
|
+
: {
|
|
5657
|
+
selector: acted.selector,
|
|
5658
|
+
description: record.actResultDescription,
|
|
5659
|
+
method: actuation.kind === "select" ? "selectOption" : "fill",
|
|
5660
|
+
arguments: [actuation.value],
|
|
5661
|
+
};
|
|
5662
|
+
return { resolvedAction };
|
|
5663
|
+
}
|
|
5664
|
+
// Failure: carry the running triedSelectors (prior xpath + this walk's own)
|
|
5665
|
+
// into the record so anyXpathResolved stays true even when every resolved
|
|
5666
|
+
// candidate was deny-listed (attemptTriedSelectors would be empty then).
|
|
5667
|
+
record.triedSelectors = [...triedSelectors];
|
|
5668
|
+
const counterStalledCount = cascadeResult.outcome?.counterStalledSelectors.length ?? 0;
|
|
5669
|
+
const failureMessage = cascadeResult.error
|
|
5670
|
+
? `${labelPrefix}: ${actuation.kind} threw ${(0, errors_1.toErrorMessage)(cascadeResult.error)}`
|
|
5671
|
+
: counterStalledCount > 0
|
|
5672
|
+
? `${labelPrefix}: clicked ${counterStalledCount} candidate(s) but the "N selected" counter never rose (no selection registered)`
|
|
5673
|
+
: attemptTriedSelectors.length > 0
|
|
5674
|
+
? `${labelPrefix}: no actionable candidate (${attemptTriedSelectors.length} not-actionable)`
|
|
5675
|
+
: `${labelPrefix}: no actionable candidate (every candidate refused by the wizard-exit deny-list)`;
|
|
5676
|
+
record.actResultSuccess = false;
|
|
5677
|
+
record.errorMessage = failureMessage;
|
|
5678
|
+
attempts.push(record);
|
|
5679
|
+
failureReasons.push(failureMessage);
|
|
5680
|
+
logger.info(`${formatStepPrefix(stepIndex, totalSteps)} attempt ${attempt}: ${failureMessage}`);
|
|
5681
|
+
return { resolvedAction: null };
|
|
5682
|
+
};
|
|
5575
5683
|
let phantomClickAfterAttempt1 = false;
|
|
5576
5684
|
for (let attempt = 1; attempt <= MAX_STEP_ATTEMPTS; attempt++) {
|
|
5577
5685
|
// Telemetry-driven technique-skip: when a cascade technique's
|
|
@@ -5583,7 +5691,9 @@ async function executeStepWithHealing(params) {
|
|
|
5583
5691
|
const wouldBeTechnique = attempt === 2
|
|
5584
5692
|
? phantomClickAfterAttempt1 && (isFinalStep || submitStep)
|
|
5585
5693
|
? "deep-submit-locator"
|
|
5586
|
-
:
|
|
5694
|
+
: phantomClickAfterAttempt1
|
|
5695
|
+
? "trusted-click-retry"
|
|
5696
|
+
: "observe-act"
|
|
5587
5697
|
: attempt === 3
|
|
5588
5698
|
? "structured-click"
|
|
5589
5699
|
: attempt === 4
|
|
@@ -5842,6 +5952,113 @@ async function executeStepWithHealing(params) {
|
|
|
5842
5952
|
}
|
|
5843
5953
|
}
|
|
5844
5954
|
}
|
|
5955
|
+
else if (attempt === 2 && phantomClickAfterAttempt1 && !(isFinalStep || submitStep)) {
|
|
5956
|
+
// Trusted-click retry: attempt 1 phantom-clicked a NON-submit control —
|
|
5957
|
+
// Stagehand reported success but pre/post showed zero effect. On a
|
|
5958
|
+
// design-system widget (React synthetic-event delegation, custom
|
|
5959
|
+
// event-bound wrappers) the likely cause is that the attempt-1 click was
|
|
5960
|
+
// an in-page, `isTrusted=false` activation the handler ignores — the
|
|
5961
|
+
// element resolves fine, but only a REAL user gesture registers. So
|
|
5962
|
+
// re-click the target with a TRUSTED gesture. Two arms by page shape:
|
|
5963
|
+
// a top-window (no frame seam) page re-clicks attempt-1's resolved xpath
|
|
5964
|
+
// via a Playwright `.locator().first().click()` on the main frame; an
|
|
5965
|
+
// OOPIF/cross-origin page re-resolves via the frame-seam deepLocator and
|
|
5966
|
+
// clicks through `clickDeepLocatorCandidate` → `deepLocator().nth().click()`
|
|
5967
|
+
// (an `Input.dispatchMouseEvent`). Both deliver `isTrusted=true`. This is
|
|
5968
|
+
// the non-submit sibling of the deep-submit-locator escalation above.
|
|
5969
|
+
record.technique = "trusted-click-retry";
|
|
5970
|
+
// Carry forward any selector prior attempts already resolved (e.g.
|
|
5971
|
+
// attempt-1 act-string's xpath) so a trusted-click-retry that can't
|
|
5972
|
+
// resolve a deepLocator candidate doesn't erase the xpath signal
|
|
5973
|
+
// structured-click's precondition (shouldSkipTechnique) depends on —
|
|
5974
|
+
// this attempt is an ADDITIONAL recovery, not a replacement that
|
|
5975
|
+
// demotes the rest of the ladder.
|
|
5976
|
+
if (!frameTarget?.frame) {
|
|
5977
|
+
// Top-window path: this wizard renders in the top document with no
|
|
5978
|
+
// cross-origin OOPIF, so `deepLocator` (which is built entirely on a
|
|
5979
|
+
// frame seam — deep-locator-candidates.ts's resolveScanFrameTarget
|
|
5980
|
+
// returns null when no frameSelector resolves) yields nothing. The
|
|
5981
|
+
// trusted-click primitive `deepLocator().nth().click()` is therefore
|
|
5982
|
+
// unreachable, but the ACTIVATION we need — a real `isTrusted=true`
|
|
5983
|
+
// gesture the Base Web handler honours — is still deliverable via a
|
|
5984
|
+
// Playwright Locator click on the top frame (the same trusted click
|
|
5985
|
+
// applyRadioSelection uses at Tier A). Re-click attempt-1's resolved
|
|
5986
|
+
// xpath through the main-frame FrameTarget's `.locator()` instead of
|
|
5987
|
+
// bailing; the synthesized click action flows through the standard
|
|
5988
|
+
// verifier exactly like the frame-seam path's result.
|
|
5989
|
+
//
|
|
5990
|
+
// FIRST xpath, not last: attempt-1's act pushes every resolved
|
|
5991
|
+
// action's selector into `triedSelectors` in order but binds the
|
|
5992
|
+
// phantom-classified `resolvedAction` to the FIRST (see the
|
|
5993
|
+
// `if (!resolvedAction)` at the attempt-1 branch). On a multi-action
|
|
5994
|
+
// attempt-1 the last entry is a different control than the one that
|
|
5995
|
+
// was clicked, so the first match is the phantomed target.
|
|
5996
|
+
const topWindowSelector = triedSelectors.find((sel) => xpathBodyForEvaluate(sel) !== null);
|
|
5997
|
+
if (!topWindowSelector) {
|
|
5998
|
+
const failureMessage = "trusted-click-retry: no top-window selector resolved for the phantomed target";
|
|
5999
|
+
record.actResultSuccess = false;
|
|
6000
|
+
record.errorMessage = failureMessage;
|
|
6001
|
+
record.triedSelectors = [...triedSelectors];
|
|
6002
|
+
attempts.push(record);
|
|
6003
|
+
failureReasons.push(failureMessage);
|
|
6004
|
+
logger.info(`${formatStepPrefix(stepIndex, totalSteps)} attempt ${attempt}: ${failureMessage}`);
|
|
6005
|
+
continue;
|
|
6006
|
+
}
|
|
6007
|
+
const topWindowTarget = frameTarget ?? (0, frame_target_1.mainFrameTarget)(page);
|
|
6008
|
+
try {
|
|
6009
|
+
await topWindowTarget.locator(topWindowSelector).first().click();
|
|
6010
|
+
}
|
|
6011
|
+
catch (err) {
|
|
6012
|
+
const failureMessage = `trusted-click-retry: top-window trusted click threw ${(0, errors_1.toErrorMessage)(err)}`;
|
|
6013
|
+
record.actResultSuccess = false;
|
|
6014
|
+
record.errorMessage = failureMessage;
|
|
6015
|
+
record.triedSelectors = [...triedSelectors];
|
|
6016
|
+
attempts.push(record);
|
|
6017
|
+
failureReasons.push(failureMessage);
|
|
6018
|
+
logger.info(`${formatStepPrefix(stepIndex, totalSteps)} attempt ${attempt}: ${failureMessage}`);
|
|
6019
|
+
continue;
|
|
6020
|
+
}
|
|
6021
|
+
record.instruction = `trusted-click-retry (top-window): ${topWindowSelector}`;
|
|
6022
|
+
record.actResultSuccess = true;
|
|
6023
|
+
record.actResultDescription = `trusted-click-retry clicked "${topWindowSelector}" via top-window locator`;
|
|
6024
|
+
record.triedSelectors = [...triedSelectors];
|
|
6025
|
+
logger.info(`${formatStepPrefix(stepIndex, totalSteps)} attempt ${attempt}: trusted-click-retry: top-window trusted locator click on the resolved target`);
|
|
6026
|
+
resolvedAction = {
|
|
6027
|
+
selector: topWindowSelector,
|
|
6028
|
+
description: record.actResultDescription,
|
|
6029
|
+
method: "click",
|
|
6030
|
+
};
|
|
6031
|
+
}
|
|
6032
|
+
else {
|
|
6033
|
+
await reresolveFrameTargetIfLost();
|
|
6034
|
+
const { candidates: retryCandidates, innerSelector: retryInnerSelector } = await resolveDeepLocatorCandidatesWithWidening(page, frameTarget.frameSelector, step, {
|
|
6035
|
+
frameTarget,
|
|
6036
|
+
});
|
|
6037
|
+
const deepLocatorCandidates = retryCandidates.filter((c) => !triedSelectors.includes(c.selector));
|
|
6038
|
+
if (deepLocatorCandidates.length === 0) {
|
|
6039
|
+
const failureMessage = "trusted-click-retry: no deepLocator candidate resolved for the phantomed target";
|
|
6040
|
+
record.actResultSuccess = false;
|
|
6041
|
+
record.errorMessage = failureMessage;
|
|
6042
|
+
record.triedSelectors = [...triedSelectors];
|
|
6043
|
+
attempts.push(record);
|
|
6044
|
+
failureReasons.push(failureMessage);
|
|
6045
|
+
logger.info(`${formatStepPrefix(stepIndex, totalSteps)} attempt ${attempt}: ${failureMessage}`);
|
|
6046
|
+
continue;
|
|
6047
|
+
}
|
|
6048
|
+
const { resolvedAction: retryResolvedAction } = await runDeepLocatorClickWalk({
|
|
6049
|
+
candidates: deepLocatorCandidates,
|
|
6050
|
+
innerSelector: retryInnerSelector,
|
|
6051
|
+
preferTrustedClick: true,
|
|
6052
|
+
labelPrefix: "trusted-click-retry",
|
|
6053
|
+
record,
|
|
6054
|
+
attempt,
|
|
6055
|
+
pre,
|
|
6056
|
+
});
|
|
6057
|
+
if (!retryResolvedAction)
|
|
6058
|
+
continue;
|
|
6059
|
+
resolvedAction = retryResolvedAction;
|
|
6060
|
+
}
|
|
6061
|
+
}
|
|
5845
6062
|
else if (attempt === 2 || attempt === 4) {
|
|
5846
6063
|
record.technique = attempt === 2 ? "observe-act" : "observe-act-exclude";
|
|
5847
6064
|
const observeOptions = attempt === 4 && triedSelectors.length > 0
|
|
@@ -5929,128 +6146,26 @@ async function executeStepWithHealing(params) {
|
|
|
5929
6146
|
continue;
|
|
5930
6147
|
}
|
|
5931
6148
|
}
|
|
5932
|
-
// Actionable-candidate walk
|
|
5933
|
-
//
|
|
5934
|
-
// node) or is refused by
|
|
5935
|
-
//
|
|
5936
|
-
// is tried instead.
|
|
5937
|
-
//
|
|
5938
|
-
//
|
|
5939
|
-
//
|
|
5940
|
-
const
|
|
5941
|
-
|
|
5942
|
-
|
|
5943
|
-
|
|
5944
|
-
|
|
5945
|
-
|
|
5946
|
-
|
|
5947
|
-
|
|
5948
|
-
|
|
5949
|
-
|
|
5950
|
-
// `target.method === "fill"` handling above); the deepLocator
|
|
5951
|
-
// fallback has no such resolved action, so the step prose is the
|
|
5952
|
-
// only place the fill/select value can come from. Derived once per
|
|
5953
|
-
// attempt (not per candidate) — every candidate in this walk is a
|
|
5954
|
-
// guess at the SAME step's target, so they all actuate the same way.
|
|
5955
|
-
const actuation = resolveDeepLocatorActuation(step);
|
|
5956
|
-
// Next-best-candidate recovery for a markerless multi-select phantom:
|
|
5957
|
-
// a click-to-toggle option (not a native <select> write, which
|
|
5958
|
-
// already read-back-verifies) whose "N selected" counter doesn't rise
|
|
5959
|
-
// means the click landed but didn't register — so the walk should try
|
|
5960
|
-
// the next real option before this attempt is scored a failure and a
|
|
5961
|
-
// replan is spent. Only wired for a selection-intent CLICK step; the
|
|
5962
|
-
// reader re-reads the same counter `pre` captured (via
|
|
5963
|
-
// `selectionCountFromSignature`) so no extra pre-snapshot is paid, and
|
|
5964
|
-
// returns `null` on any frame hiccup so a read failure never converts
|
|
5965
|
-
// to a walk-stopping throw. Non-selection steps pass `undefined` and
|
|
5966
|
-
// the walk behaves exactly as before.
|
|
5967
|
-
const isSelectionIntentClick = actuation.kind === "click" && parseSelectStep(step) !== null;
|
|
5968
|
-
const baselineSelectionCount = isSelectionIntentClick
|
|
5969
|
-
? selectionCountFromSignature(pre.visibleTextSignature)
|
|
5970
|
-
: null;
|
|
5971
|
-
const readSelectionCount = isSelectionIntentClick
|
|
5972
|
-
? async () => {
|
|
5973
|
-
try {
|
|
5974
|
-
const snap = await snapshotPage(frameTarget ?? (0, frame_target_1.mainFrameTarget)(page), signalCounter, page);
|
|
5975
|
-
return selectionCountFromSignature(snap.visibleTextSignature);
|
|
5976
|
-
}
|
|
5977
|
-
catch {
|
|
5978
|
-
return null;
|
|
5979
|
-
}
|
|
5980
|
-
}
|
|
5981
|
-
: undefined;
|
|
5982
|
-
const cascadeResult = await (0, deep_locator_click_1.clickFirstActionableCandidate)(deepLocatorCandidates, async (candidate) => {
|
|
5983
|
-
attemptTriedSelectors.push(candidate.selector);
|
|
5984
|
-
if (actuation.kind === "select") {
|
|
5985
|
-
const verified = await (0, deep_locator_actuate_1.selectDeepLocatorCandidateOption)(page, frameTarget?.frameSelector, deepLocatorInnerSelector, candidate.index, actuation.value, { frameTarget });
|
|
5986
|
-
// selectDeepLocatorCandidateOption never throws on an ordinary
|
|
5987
|
-
// failed write (see deep-locator-actuate.ts's writeAndVerify) —
|
|
5988
|
-
// it resolves `false` for both a rejected selectOption() and a
|
|
5989
|
-
// read-back mismatch. clickFirstActionableCandidate infers
|
|
5990
|
-
// success from "didn't throw", so a `false` here must become a
|
|
5991
|
-
// throw or the walk would wrongly report this candidate as
|
|
5992
|
-
// actuated.
|
|
5993
|
-
if (!verified)
|
|
5994
|
-
throw new Error("-32000 Node does not have a layout object");
|
|
5995
|
-
}
|
|
5996
|
-
else if (actuation.kind === "fill") {
|
|
5997
|
-
const verified = await (0, deep_locator_actuate_1.fillDeepLocatorCandidate)(page, frameTarget?.frameSelector, deepLocatorInnerSelector, candidate.index, actuation.value, { frameTarget });
|
|
5998
|
-
if (!verified)
|
|
5999
|
-
throw new Error("-32000 Node does not have a layout object");
|
|
6000
|
-
}
|
|
6001
|
-
else {
|
|
6002
|
-
await (0, deep_locator_candidates_1.clickDeepLocatorCandidate)(page, frameTarget?.frameSelector, deepLocatorInnerSelector, candidate.index, { frameTarget });
|
|
6003
|
-
}
|
|
6004
|
-
}, { denyCandidate, readSelectionCount, baselineSelectionCount })
|
|
6005
|
-
.then((outcome) => ({ outcome, error: null }))
|
|
6006
|
-
.catch((error) => ({ outcome: null, error }));
|
|
6007
|
-
triedSelectors.push(...attemptTriedSelectors);
|
|
6008
|
-
record.triedSelectors = [...attemptTriedSelectors];
|
|
6009
|
-
if (cascadeResult.outcome?.clicked && cascadeResult.outcome.candidate) {
|
|
6010
|
-
const acted = cascadeResult.outcome.candidate;
|
|
6011
|
-
const verb = actuation.kind === "select"
|
|
6012
|
-
? "selected"
|
|
6013
|
-
: actuation.kind === "fill"
|
|
6014
|
-
? "filled"
|
|
6015
|
-
: "clicked";
|
|
6016
|
-
record.instruction = `deepLocator: ${acted.accessibleText || "(no accessible text)"}`;
|
|
6017
|
-
record.actResultSuccess = true;
|
|
6018
|
-
record.actResultDescription = `deepLocator ${verb} "${acted.accessibleText || acted.selector}"`;
|
|
6019
|
-
// Synthesize an action matching the actuation kind so downstream
|
|
6020
|
-
// verification (network/url/dom — STATE_CLASS_METHODS treats
|
|
6021
|
-
// fill/selectOption as DOM-verifiable, not click) treats this
|
|
6022
|
-
// exactly like any other resolved action — same idiom as
|
|
6023
|
-
// deep-submit-locator/structured-click.
|
|
6024
|
-
resolvedAction =
|
|
6025
|
-
actuation.kind === "click"
|
|
6026
|
-
? {
|
|
6027
|
-
selector: acted.selector,
|
|
6028
|
-
description: record.actResultDescription,
|
|
6029
|
-
method: "click",
|
|
6030
|
-
}
|
|
6031
|
-
: {
|
|
6032
|
-
selector: acted.selector,
|
|
6033
|
-
description: record.actResultDescription,
|
|
6034
|
-
method: actuation.kind === "select" ? "selectOption" : "fill",
|
|
6035
|
-
arguments: [actuation.value],
|
|
6036
|
-
};
|
|
6037
|
-
}
|
|
6038
|
-
else {
|
|
6039
|
-
const counterStalledCount = cascadeResult.outcome?.counterStalledSelectors.length ?? 0;
|
|
6040
|
-
const failureMessage = cascadeResult.error
|
|
6041
|
-
? `deepLocator: ${actuation.kind} threw ${(0, errors_1.toErrorMessage)(cascadeResult.error)}`
|
|
6042
|
-
: counterStalledCount > 0
|
|
6043
|
-
? `deepLocator: clicked ${counterStalledCount} candidate(s) but the "N selected" counter never rose (no selection registered)`
|
|
6044
|
-
: attemptTriedSelectors.length > 0
|
|
6045
|
-
? `deepLocator: no actionable candidate (${attemptTriedSelectors.length} not-actionable)`
|
|
6046
|
-
: "deepLocator: no actionable candidate (every candidate refused by the wizard-exit deny-list)";
|
|
6047
|
-
record.actResultSuccess = false;
|
|
6048
|
-
record.errorMessage = failureMessage;
|
|
6049
|
-
attempts.push(record);
|
|
6050
|
-
failureReasons.push(failureMessage);
|
|
6051
|
-
logger.info(`${formatStepPrefix(stepIndex, totalSteps)} attempt ${attempt}: ${failureMessage}`);
|
|
6149
|
+
// Actionable-candidate walk (shared with the trusted-click-retry
|
|
6150
|
+
// branch): a top pick that rejects with the CDP `-32000 Node does not
|
|
6151
|
+
// have a layout object` error (an unrendered node) or is refused by
|
|
6152
|
+
// the wizard-exit deny-list costs only that one candidate, not the
|
|
6153
|
+
// whole attempt — the next ranked candidate is tried instead. The
|
|
6154
|
+
// observe-act path activates via the synthetic fast click
|
|
6155
|
+
// (preferTrustedClick: false); the trusted CDP click is the
|
|
6156
|
+
// phantom-retry escalation.
|
|
6157
|
+
const { resolvedAction: walkResolvedAction } = await runDeepLocatorClickWalk({
|
|
6158
|
+
candidates: deepLocatorCandidates,
|
|
6159
|
+
innerSelector: deepLocatorInnerSelector,
|
|
6160
|
+
preferTrustedClick: false,
|
|
6161
|
+
labelPrefix: "deepLocator",
|
|
6162
|
+
record,
|
|
6163
|
+
attempt,
|
|
6164
|
+
pre,
|
|
6165
|
+
});
|
|
6166
|
+
if (!walkResolvedAction)
|
|
6052
6167
|
continue;
|
|
6053
|
-
|
|
6168
|
+
resolvedAction = walkResolvedAction;
|
|
6054
6169
|
}
|
|
6055
6170
|
else if (candidates.length === 0) {
|
|
6056
6171
|
record.errorMessage = "observe returned no candidates";
|
|
@@ -7149,7 +7264,7 @@ async function executeStepWithHealing(params) {
|
|
|
7149
7264
|
const suppressedCount = getSuppressedAisdkElementIdErrorCount?.();
|
|
7150
7265
|
const escalationTarget = isFinalStep || submitStep
|
|
7151
7266
|
? "escalating attempt 2 to deep-submit-locator"
|
|
7152
|
-
: "non-submit step —
|
|
7267
|
+
: "non-submit step — escalating attempt 2 to trusted-click-retry (trusted CDP click on the resolved target)";
|
|
7153
7268
|
logger.warn(`${formatStepPrefix(stepIndex, totalSteps)} phantom click detected on attempt 1 (${record.technique}): reported success with no network/url/dom change${suppressedCount !== undefined ? `; ${suppressedCount} AISDK elementId errors suppressed this session (corroborating, not causal)` : ""} — ${escalationTarget}`);
|
|
7154
7269
|
}
|
|
7155
7270
|
const postAttemptInvalidCount = await countNgInvalidContainers(frameTarget ?? (0, frame_target_1.mainFrameTarget)(page));
|