@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
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Frame-scoped candidate-scan seam: `resolveDeepLocatorCandidates`
|
|
3
|
+
* (`deep-locator-candidates.ts`) pays one CDP round-trip per candidate via
|
|
4
|
+
* `delegate.nth(index).textContent()`, because Stagehand 3.7.0's
|
|
5
|
+
* `FrameSelectorResolver.resolveAll` re-resolves the selector from index 0
|
|
6
|
+
* on every call (understudy/selectorResolver.js) — enumerating n candidates
|
|
7
|
+
* costs n(n+1)/2 round-trips, not n. Through Browserbase's proxied CDP into
|
|
8
|
+
* a cross-origin OOPIF, that measured out to ~4.6s per candidate — 371
|
|
9
|
+
* candidates never finish inside any reasonable budget.
|
|
10
|
+
*
|
|
11
|
+
* {@link buildScanFrameCandidatesExpr} is the fix: one `Frame.evaluate` call
|
|
12
|
+
* that reads accessible-name + layout/visibility for EVERY match of an inner
|
|
13
|
+
* selector inside the frame's own document, in a single CDP round-trip. The
|
|
14
|
+
* accessible name (not bare `textContent`, which the DOM spec always leaves
|
|
15
|
+
* empty for `input`/`select`/`textarea`) is what lets an `INTERACTIVE_CANDIDATE_SELECTOR`
|
|
16
|
+
* match like a labelled `<input>` surface something the rephrase LLM and
|
|
17
|
+
* `scoreCandidate` can actually compare against a step instruction.
|
|
18
|
+
* {@link buildClickFrameCandidateExpr} is the actuation half of Issue #2
|
|
19
|
+
* (clicking an unrendered node): a second one-round-trip evaluate call that
|
|
20
|
+
* re-runs the same `querySelectorAll(innerSelector)` resolution, clicks the
|
|
21
|
+
* element at a scan-derived `index`, and reports out-of-range /
|
|
22
|
+
* not-actionable outcomes as data instead of throwing a CDP `-32000` error.
|
|
23
|
+
* {@link isNodeNotActionableError} is the companion predicate for the case
|
|
24
|
+
* where a click is attempted through a different path (e.g. Stagehand's own
|
|
25
|
+
* `deepLocator().click()`) and throws anyway — both live here because a
|
|
26
|
+
* caller enumerates with the first and needs the second or third to
|
|
27
|
+
* interpret a click outcome against whatever the scan already reported as
|
|
28
|
+
* `visible`.
|
|
29
|
+
*
|
|
30
|
+
* {@link buildFillFrameCandidateExpr} and {@link buildSelectFrameCandidateExpr}
|
|
31
|
+
* extend the same one-round-trip actuation seam to the two write primitives
|
|
32
|
+
* `clickDeepLocatorCandidate` never needed: today
|
|
33
|
+
* `fillDeepLocatorCandidate`/`selectDeepLocatorCandidateOption`
|
|
34
|
+
* (`deep-locator-actuate.ts`) still pay a `deepLocator().nth(index).fill()`
|
|
35
|
+
* (or `.selectOption()`) round-trip PLUS a separate `.inputValue()`
|
|
36
|
+
* round-trip to confirm the write — each individually as expensive as
|
|
37
|
+
* `clickDeepLocatorCandidate`'s pre-fix per-index resolve. These two
|
|
38
|
+
* builders collapse write + read-back into the same evaluate call, out-of-
|
|
39
|
+
* range/not-actionable reported as data exactly like the click builder.
|
|
40
|
+
*/
|
|
41
|
+
/**
|
|
42
|
+
* CSS selector for the interactive-element universe the deepLocator cascade
|
|
43
|
+
* should enumerate instead of `"*"`, so a candidate set that would otherwise
|
|
44
|
+
* be hundreds of structural nodes (`html`, `body`, every wrapping `div`) is
|
|
45
|
+
* instead a handful of clickable controls. Exported as the single definition
|
|
46
|
+
* both the cascade and its tests share. Safe as the final `buildHopSelector`
|
|
47
|
+
* segment — `resolveLocatorTarget` only ever splits hop notation on `">>"`
|
|
48
|
+
* (understudy/deepLocator.js), so this selector's commas pass through intact.
|
|
49
|
+
*/
|
|
50
|
+
export declare const INTERACTIVE_CANDIDATE_SELECTOR = "button, a, input, select, textarea, [role=button], [tabindex]";
|
|
51
|
+
/**
|
|
52
|
+
* Additional per-CDP-round-trip watchdog budget charged per candidate
|
|
53
|
+
* `index`, on top of a call's own `callTimeoutMs`, for every legacy
|
|
54
|
+
* `deepLocator(hop).nth(index)` delegate fallback — `clickDeepLocatorCandidate`
|
|
55
|
+
* (`deep-locator-candidates.ts`) and `fillDeepLocatorCandidate`/
|
|
56
|
+
* `selectDeepLocatorCandidateOption` (`deep-locator-actuate.ts`) alike.
|
|
57
|
+
* Stagehand's `FrameSelectorResolver.resolveAtIndex(query, i)` resolves
|
|
58
|
+
* `Locator.nth(i)` via `resolveAll(query, {limit: i + 1})`, whose
|
|
59
|
+
* `resolveCss` loops one serial `Runtime.evaluate` round-trip per index up
|
|
60
|
+
* to and including `i` (understudy/selectorResolver.js:70,79-115) before
|
|
61
|
+
* ANY `.nth(i)`-chained method (`click()`, `fill()`, `selectOption()`,
|
|
62
|
+
* `inputValue()`) ever dispatches — so acting at index `i` costs `i + 1`
|
|
63
|
+
* round-trips, not one, and a fixed `callTimeoutMs` (which only ever
|
|
64
|
+
* budgeted a single round-trip) starves any candidate past the index where
|
|
65
|
+
* `(i + 1) * measuredRoundTripMs` exceeds it (measured ~0.66s/round-trip
|
|
66
|
+
* through Browserbase's proxied CDP into a live cross-origin OOPIF — run-7:
|
|
67
|
+
* candidate 13 enumerated within a 60s budget, i.e. 91 cumulative
|
|
68
|
+
* round-trips). `callTimeoutMs` already covers the first round-trip; this
|
|
69
|
+
* constant is the budget added per each of the remaining `index` round-trips,
|
|
70
|
+
* rounded up from the measured cost to leave headroom for CDP jitter. Hoisted
|
|
71
|
+
* here (rather than left module-private to `deep-locator-candidates.ts`) so
|
|
72
|
+
* `deep-locator-actuate.ts` can reuse it without importing
|
|
73
|
+
* `deep-locator-candidates.ts` or `flow-runner.ts` (import-cycle risk).
|
|
74
|
+
*/
|
|
75
|
+
export declare const DEEP_LOCATOR_CLICK_INDEX_ROUND_TRIP_MS = 1000;
|
|
76
|
+
/**
|
|
77
|
+
* Builds a self-contained evaluate expression that queries `root` for every
|
|
78
|
+
* match of `innerSelector` via `document.querySelectorAll` — the SAME
|
|
79
|
+
* resolution Stagehand's own primary CSS resolver uses
|
|
80
|
+
* (`resolveCssSelector(sel, i) = querySelectorAll(sel)[i]`, in the frame's
|
|
81
|
+
* own document) — and returns one entry per match, in document order:
|
|
82
|
+
* `{ index, text, visible }`. `index` therefore lines up 1:1 with the index
|
|
83
|
+
* `deepLocator(hop).nth(index)` will later resolve to, so a caller can scan
|
|
84
|
+
* once here and hand an index to the existing per-index click path without
|
|
85
|
+
* re-deriving the candidate set.
|
|
86
|
+
*
|
|
87
|
+
* That alignment diverges only when the light-DOM query matches fewer than
|
|
88
|
+
* `i + 1` elements, which is what triggers Stagehand's shadow-piercing
|
|
89
|
+
* `resolveCssSelectorPierce` fallback instead of the plain CSS resolver —
|
|
90
|
+
* `innerSelector` should stay a plain, non-shadow-crossing selector (e.g.
|
|
91
|
+
* {@link INTERACTIVE_CANDIDATE_SELECTOR}) whenever a caller depends on index
|
|
92
|
+
* alignment with `deepLocator`.
|
|
93
|
+
*
|
|
94
|
+
* `root` overrides the traversal root expression (default `"document"`),
|
|
95
|
+
* interpolated verbatim into the generated code so a caller evaluating this
|
|
96
|
+
* expression via `Frame.evaluate` still resolves that frame's own document —
|
|
97
|
+
* the expression never captures an outer `document` reference (mirrors
|
|
98
|
+
* `buildRankSubmitCandidatesExpr`'s contract in `submit-control.ts`).
|
|
99
|
+
*/
|
|
100
|
+
export declare function buildScanFrameCandidatesExpr(innerSelector: string, root?: string): string;
|
|
101
|
+
/** One candidate {@link buildScanFrameCandidatesExpr}'s evaluate call returns. */
|
|
102
|
+
export interface FrameCandidateScanResult {
|
|
103
|
+
/** Position in `querySelectorAll(innerSelector)`'s match order — aligns with `deepLocator(hop).nth(index)` and with {@link buildClickFrameCandidateExpr}'s `index` argument, subject to the shadow-piercing divergence documented on {@link buildScanFrameCandidatesExpr}. */
|
|
104
|
+
index: number;
|
|
105
|
+
/** The element's derived accessible name (see {@link buildAccessibleNameExpr}'s precedence), untrimmed at the edges the precedence chain doesn't already trim — `deep-locator-candidates.ts`'s `scanFrameCandidatesBatched` `.trim()`s it before use. */
|
|
106
|
+
text: string;
|
|
107
|
+
/** `false` when the element has a 0x0 layout box or a computed `display:none`/`visibility:hidden` style — never actionable via a real click. */
|
|
108
|
+
visible: boolean;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Builds a self-contained evaluate expression that re-runs the SAME
|
|
112
|
+
* `root.querySelectorAll(innerSelector)` resolution {@link buildScanFrameCandidatesExpr}
|
|
113
|
+
* uses and clicks the element at `index` (dispatching focus + bubbling
|
|
114
|
+
* mousedown/mouseup/click, matching `buildClickByDeepIndexExpr`'s
|
|
115
|
+
* (`submit-control.ts`) controlled-state click convention). This is the
|
|
116
|
+
* one-round-trip actuation half of the batched-scan fix: a caller that
|
|
117
|
+
* scanned via {@link buildScanFrameCandidatesExpr} can hand the chosen
|
|
118
|
+
* `index` straight to this builder without re-deriving the candidate set,
|
|
119
|
+
* because both builders resolve `querySelectorAll(innerSelector)` against
|
|
120
|
+
* the same frame document in the same match order.
|
|
121
|
+
*
|
|
122
|
+
* Reports out-of-range / not-actionable outcomes as data rather than by
|
|
123
|
+
* throwing, mirroring {@link isNodeNotActionableError}'s CDP-error-side
|
|
124
|
+
* contract from the DOM-observable side:
|
|
125
|
+
* - `index` no longer matches (e.g. the DOM changed between scan and click)
|
|
126
|
+
* returns `{ clicked: false, reason: "out-of-range" }`.
|
|
127
|
+
* - the matched element fails {@link IS_VISIBLE_EXPR} — the same
|
|
128
|
+
* visibility check {@link buildScanFrameCandidatesExpr} reports as
|
|
129
|
+
* `visible: false` — returns `{ clicked: false, reason: "not-actionable" }`
|
|
130
|
+
* instead of dispatching a click a real browser would reject with a
|
|
131
|
+
* `-32000 Node does not have a layout object` CDP error.
|
|
132
|
+
*
|
|
133
|
+
* `root` overrides the traversal root expression (default `"document"`) and
|
|
134
|
+
* must match the `root` passed to the {@link buildScanFrameCandidatesExpr}
|
|
135
|
+
* call that produced `index`, or the re-run query will not resolve against
|
|
136
|
+
* the same document. Interpolated verbatim so a caller evaluating this
|
|
137
|
+
* expression via `Frame.evaluate` still resolves that frame's own document —
|
|
138
|
+
* the expression never captures an outer `document` reference.
|
|
139
|
+
*/
|
|
140
|
+
export declare function buildClickFrameCandidateExpr(innerSelector: string, index: number, root?: string): string;
|
|
141
|
+
/** Reason {@link buildClickFrameCandidateExpr} reports when it returns `{ clicked: false }` instead of throwing. */
|
|
142
|
+
export type FrameCandidateClickSkipReason = "out-of-range" | "not-actionable";
|
|
143
|
+
/** Result of {@link buildClickFrameCandidateExpr}'s evaluate call. */
|
|
144
|
+
export interface FrameCandidateClickResult {
|
|
145
|
+
clicked: boolean;
|
|
146
|
+
/** Present only when `clicked` is `false` — distinguishes a stale index from an unrendered element. */
|
|
147
|
+
reason?: FrameCandidateClickSkipReason;
|
|
148
|
+
}
|
|
149
|
+
/** Reason {@link buildFillFrameCandidateExpr}/{@link buildSelectFrameCandidateExpr} report when they return `{ written: false }` instead of throwing — same two reasons {@link FrameCandidateClickSkipReason} reports for a click, since both re-run the identical resolve-and-check-visibility steps. */
|
|
150
|
+
export type FrameCandidateWriteSkipReason = "out-of-range" | "not-actionable";
|
|
151
|
+
/** Result of {@link buildFillFrameCandidateExpr}'s or {@link buildSelectFrameCandidateExpr}'s evaluate call. */
|
|
152
|
+
export interface FrameCandidateWriteResult {
|
|
153
|
+
written: boolean;
|
|
154
|
+
/** Present only when `written` is `true` — the value read back from the element immediately after the write, so a caller can compare it against what it asked for without a second round-trip. For the select expression this is the MATCHED option's `value`, not necessarily the (possibly label) string the caller passed in. */
|
|
155
|
+
readBack?: string;
|
|
156
|
+
/** Present only when `written` is `false` — distinguishes a stale index from an element the write could not land on. */
|
|
157
|
+
reason?: FrameCandidateWriteSkipReason;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Builds a self-contained evaluate expression that fills the `<input>`/
|
|
161
|
+
* `<textarea>` at `root.querySelectorAll(innerSelector)[index]` with
|
|
162
|
+
* `value` — the one-round-trip write half of
|
|
163
|
+
* {@link fillDeepLocatorCandidate}'s (`deep-locator-actuate.ts`) contract.
|
|
164
|
+
* See {@link buildWriteFrameCandidateExpr} for the shared write/dispatch/
|
|
165
|
+
* read-back mechanism and out-of-range/not-actionable reporting. Resolves
|
|
166
|
+
* the native value-setter descriptor off `HTMLTextAreaElement.prototype`
|
|
167
|
+
* when the matched element's own `tagName` is `"textarea"`, else
|
|
168
|
+
* `HTMLInputElement.prototype` — determined at evaluate-time (inside the
|
|
169
|
+
* generated code), since `innerSelector` may match a mix of input and
|
|
170
|
+
* textarea nodes (e.g. {@link INTERACTIVE_CANDIDATE_SELECTOR}).
|
|
171
|
+
*
|
|
172
|
+
* `root` overrides the traversal root expression (default `"document"`) and
|
|
173
|
+
* must match the `root` a prior {@link buildScanFrameCandidatesExpr} call
|
|
174
|
+
* used to derive `index`, mirroring
|
|
175
|
+
* {@link buildClickFrameCandidateExpr}'s `root` contract.
|
|
176
|
+
*/
|
|
177
|
+
export declare function buildFillFrameCandidateExpr(innerSelector: string, index: number, value: string, root?: string): string;
|
|
178
|
+
/**
|
|
179
|
+
* Builds a self-contained evaluate expression that re-runs the SAME
|
|
180
|
+
* `root.querySelectorAll(innerSelector)` resolution and selects, on the
|
|
181
|
+
* `<select>`-shaped candidate at `index`, the option whose `value` — or,
|
|
182
|
+
* failing that, whose trimmed visible label — matches `value`. Matching by
|
|
183
|
+
* value first mirrors what a `<select>`'s own `value` property actually is
|
|
184
|
+
* (the matched option's `value` attribute); falling back to the trimmed
|
|
185
|
+
* label covers a flow instruction that quotes the option's VISIBLE label
|
|
186
|
+
* instead (`parseSelectStep`), which a `<select>`'s DOM `value` never is —
|
|
187
|
+
* the same value-then-label tolerance `trySelectPrimitive`'s deterministic
|
|
188
|
+
* match already applies across every `<select>` on the page
|
|
189
|
+
* (`flow-runner.ts:3576`), reproduced here inline because this expression
|
|
190
|
+
* matches options within one already-chosen candidate rather than
|
|
191
|
+
* enumerating the whole page. See {@link buildWriteFrameCandidateExpr} for
|
|
192
|
+
* the write/dispatch/read-back mechanism this reimplements to accommodate
|
|
193
|
+
* the option lookup — the shared helper alone can't express "write the
|
|
194
|
+
* MATCHED option's value, not the caller's raw string."
|
|
195
|
+
*
|
|
196
|
+
* `root` overrides the traversal root expression (default `"document"`) and
|
|
197
|
+
* must match the `root` a prior {@link buildScanFrameCandidatesExpr} call
|
|
198
|
+
* used to derive `index`, mirroring
|
|
199
|
+
* {@link buildClickFrameCandidateExpr}'s `root` contract. A candidate with
|
|
200
|
+
* no option matching `value` by either value or label reports
|
|
201
|
+
* `{ written: false, reason: "not-actionable" }` — nothing on the page can
|
|
202
|
+
* satisfy the write.
|
|
203
|
+
*/
|
|
204
|
+
export declare function buildSelectFrameCandidateExpr(innerSelector: string, index: number, value: string, root?: string): string;
|
|
205
|
+
/**
|
|
206
|
+
* Detects the CDP "node is not actionable" failure shape so a caller
|
|
207
|
+
* cascading through candidates can treat it as "skip this candidate, try the
|
|
208
|
+
* next" instead of scoring the attempt as a failed click. Covers both
|
|
209
|
+
* surfaces a click against an unrendered node can throw through: a raw CDP
|
|
210
|
+
* protocol error rejects as `new Error(`${code} ${message}`)`
|
|
211
|
+
* (understudy/cdp.js), so a `DOM.getBoxModel`/`DOM.scrollIntoViewIfNeeded`
|
|
212
|
+
* failure over "no layout object" arrives as a plain `Error` whose message is
|
|
213
|
+
* exactly `-32000 Node does not have a layout object`; separately,
|
|
214
|
+
* `Locator.click()`/`Locator.hover()` (understudy/locator.js) throw
|
|
215
|
+
* `ElementNotVisibleError` directly when `DOM.getBoxModel` returns no model.
|
|
216
|
+
* A name/substring check on both shapes is sufficient — no new error class
|
|
217
|
+
* is needed.
|
|
218
|
+
*/
|
|
219
|
+
export declare function isNodeNotActionableError(error: unknown): boolean;
|
|
220
|
+
//# sourceMappingURL=deep-locator-scan.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deep-locator-scan.d.ts","sourceRoot":"","sources":["../../src/scraper/deep-locator-scan.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAEH;;;;;;;;GAQG;AACH,eAAO,MAAM,8BAA8B,kEACsB,CAAC;AAElE;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,sCAAsC,OAAQ,CAAC;AA4F5D;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,4BAA4B,CAAC,aAAa,EAAE,MAAM,EAAE,IAAI,SAAa,GAAG,MAAM,CAW7F;AAED,kFAAkF;AAClF,MAAM,WAAW,wBAAwB;IACvC,8QAA8Q;IAC9Q,KAAK,EAAE,MAAM,CAAC;IACd,yPAAyP;IACzP,IAAI,EAAE,MAAM,CAAC;IACb,gJAAgJ;IAChJ,OAAO,EAAE,OAAO,CAAC;CAClB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,4BAA4B,CAC1C,aAAa,EAAE,MAAM,EACrB,KAAK,EAAE,MAAM,EACb,IAAI,SAAa,GAChB,MAAM,CAaR;AAED,oHAAoH;AACpH,MAAM,MAAM,6BAA6B,GAAG,cAAc,GAAG,gBAAgB,CAAC;AAE9E,sEAAsE;AACtE,MAAM,WAAW,yBAAyB;IACxC,OAAO,EAAE,OAAO,CAAC;IACjB,uGAAuG;IACvG,MAAM,CAAC,EAAE,6BAA6B,CAAC;CACxC;AAED,0SAA0S;AAC1S,MAAM,MAAM,6BAA6B,GAAG,cAAc,GAAG,gBAAgB,CAAC;AAE9E,gHAAgH;AAChH,MAAM,WAAW,yBAAyB;IACxC,OAAO,EAAE,OAAO,CAAC;IACjB,oUAAoU;IACpU,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,wHAAwH;IACxH,MAAM,CAAC,EAAE,6BAA6B,CAAC;CACxC;AAoDD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,2BAA2B,CACzC,aAAa,EAAE,MAAM,EACrB,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,EACb,IAAI,SAAa,GAChB,MAAM,CAIR;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,6BAA6B,CAC3C,aAAa,EAAE,MAAM,EACrB,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,EACb,IAAI,SAAa,GAChB,MAAM,CAqBR;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAIhE"}
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Frame-scoped candidate-scan seam: `resolveDeepLocatorCandidates`
|
|
4
|
+
* (`deep-locator-candidates.ts`) pays one CDP round-trip per candidate via
|
|
5
|
+
* `delegate.nth(index).textContent()`, because Stagehand 3.7.0's
|
|
6
|
+
* `FrameSelectorResolver.resolveAll` re-resolves the selector from index 0
|
|
7
|
+
* on every call (understudy/selectorResolver.js) — enumerating n candidates
|
|
8
|
+
* costs n(n+1)/2 round-trips, not n. Through Browserbase's proxied CDP into
|
|
9
|
+
* a cross-origin OOPIF, that measured out to ~4.6s per candidate — 371
|
|
10
|
+
* candidates never finish inside any reasonable budget.
|
|
11
|
+
*
|
|
12
|
+
* {@link buildScanFrameCandidatesExpr} is the fix: one `Frame.evaluate` call
|
|
13
|
+
* that reads accessible-name + layout/visibility for EVERY match of an inner
|
|
14
|
+
* selector inside the frame's own document, in a single CDP round-trip. The
|
|
15
|
+
* accessible name (not bare `textContent`, which the DOM spec always leaves
|
|
16
|
+
* empty for `input`/`select`/`textarea`) is what lets an `INTERACTIVE_CANDIDATE_SELECTOR`
|
|
17
|
+
* match like a labelled `<input>` surface something the rephrase LLM and
|
|
18
|
+
* `scoreCandidate` can actually compare against a step instruction.
|
|
19
|
+
* {@link buildClickFrameCandidateExpr} is the actuation half of Issue #2
|
|
20
|
+
* (clicking an unrendered node): a second one-round-trip evaluate call that
|
|
21
|
+
* re-runs the same `querySelectorAll(innerSelector)` resolution, clicks the
|
|
22
|
+
* element at a scan-derived `index`, and reports out-of-range /
|
|
23
|
+
* not-actionable outcomes as data instead of throwing a CDP `-32000` error.
|
|
24
|
+
* {@link isNodeNotActionableError} is the companion predicate for the case
|
|
25
|
+
* where a click is attempted through a different path (e.g. Stagehand's own
|
|
26
|
+
* `deepLocator().click()`) and throws anyway — both live here because a
|
|
27
|
+
* caller enumerates with the first and needs the second or third to
|
|
28
|
+
* interpret a click outcome against whatever the scan already reported as
|
|
29
|
+
* `visible`.
|
|
30
|
+
*
|
|
31
|
+
* {@link buildFillFrameCandidateExpr} and {@link buildSelectFrameCandidateExpr}
|
|
32
|
+
* extend the same one-round-trip actuation seam to the two write primitives
|
|
33
|
+
* `clickDeepLocatorCandidate` never needed: today
|
|
34
|
+
* `fillDeepLocatorCandidate`/`selectDeepLocatorCandidateOption`
|
|
35
|
+
* (`deep-locator-actuate.ts`) still pay a `deepLocator().nth(index).fill()`
|
|
36
|
+
* (or `.selectOption()`) round-trip PLUS a separate `.inputValue()`
|
|
37
|
+
* round-trip to confirm the write — each individually as expensive as
|
|
38
|
+
* `clickDeepLocatorCandidate`'s pre-fix per-index resolve. These two
|
|
39
|
+
* builders collapse write + read-back into the same evaluate call, out-of-
|
|
40
|
+
* range/not-actionable reported as data exactly like the click builder.
|
|
41
|
+
*/
|
|
42
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
|
+
exports.DEEP_LOCATOR_CLICK_INDEX_ROUND_TRIP_MS = exports.INTERACTIVE_CANDIDATE_SELECTOR = void 0;
|
|
44
|
+
exports.buildScanFrameCandidatesExpr = buildScanFrameCandidatesExpr;
|
|
45
|
+
exports.buildClickFrameCandidateExpr = buildClickFrameCandidateExpr;
|
|
46
|
+
exports.buildFillFrameCandidateExpr = buildFillFrameCandidateExpr;
|
|
47
|
+
exports.buildSelectFrameCandidateExpr = buildSelectFrameCandidateExpr;
|
|
48
|
+
exports.isNodeNotActionableError = isNodeNotActionableError;
|
|
49
|
+
/**
|
|
50
|
+
* CSS selector for the interactive-element universe the deepLocator cascade
|
|
51
|
+
* should enumerate instead of `"*"`, so a candidate set that would otherwise
|
|
52
|
+
* be hundreds of structural nodes (`html`, `body`, every wrapping `div`) is
|
|
53
|
+
* instead a handful of clickable controls. Exported as the single definition
|
|
54
|
+
* both the cascade and its tests share. Safe as the final `buildHopSelector`
|
|
55
|
+
* segment — `resolveLocatorTarget` only ever splits hop notation on `">>"`
|
|
56
|
+
* (understudy/deepLocator.js), so this selector's commas pass through intact.
|
|
57
|
+
*/
|
|
58
|
+
exports.INTERACTIVE_CANDIDATE_SELECTOR = "button, a, input, select, textarea, [role=button], [tabindex]";
|
|
59
|
+
/**
|
|
60
|
+
* Additional per-CDP-round-trip watchdog budget charged per candidate
|
|
61
|
+
* `index`, on top of a call's own `callTimeoutMs`, for every legacy
|
|
62
|
+
* `deepLocator(hop).nth(index)` delegate fallback — `clickDeepLocatorCandidate`
|
|
63
|
+
* (`deep-locator-candidates.ts`) and `fillDeepLocatorCandidate`/
|
|
64
|
+
* `selectDeepLocatorCandidateOption` (`deep-locator-actuate.ts`) alike.
|
|
65
|
+
* Stagehand's `FrameSelectorResolver.resolveAtIndex(query, i)` resolves
|
|
66
|
+
* `Locator.nth(i)` via `resolveAll(query, {limit: i + 1})`, whose
|
|
67
|
+
* `resolveCss` loops one serial `Runtime.evaluate` round-trip per index up
|
|
68
|
+
* to and including `i` (understudy/selectorResolver.js:70,79-115) before
|
|
69
|
+
* ANY `.nth(i)`-chained method (`click()`, `fill()`, `selectOption()`,
|
|
70
|
+
* `inputValue()`) ever dispatches — so acting at index `i` costs `i + 1`
|
|
71
|
+
* round-trips, not one, and a fixed `callTimeoutMs` (which only ever
|
|
72
|
+
* budgeted a single round-trip) starves any candidate past the index where
|
|
73
|
+
* `(i + 1) * measuredRoundTripMs` exceeds it (measured ~0.66s/round-trip
|
|
74
|
+
* through Browserbase's proxied CDP into a live cross-origin OOPIF — run-7:
|
|
75
|
+
* candidate 13 enumerated within a 60s budget, i.e. 91 cumulative
|
|
76
|
+
* round-trips). `callTimeoutMs` already covers the first round-trip; this
|
|
77
|
+
* constant is the budget added per each of the remaining `index` round-trips,
|
|
78
|
+
* rounded up from the measured cost to leave headroom for CDP jitter. Hoisted
|
|
79
|
+
* here (rather than left module-private to `deep-locator-candidates.ts`) so
|
|
80
|
+
* `deep-locator-actuate.ts` can reuse it without importing
|
|
81
|
+
* `deep-locator-candidates.ts` or `flow-runner.ts` (import-cycle risk).
|
|
82
|
+
*/
|
|
83
|
+
exports.DEEP_LOCATOR_CLICK_INDEX_ROUND_TRIP_MS = 1_000;
|
|
84
|
+
/**
|
|
85
|
+
* Visibility check shared by every candidate the scan expression builds: a
|
|
86
|
+
* node with a 0x0 layout box, or a computed `display:none`/
|
|
87
|
+
* `visibility:hidden`, can never be the target of a real click — this is the
|
|
88
|
+
* DOM-observable symptom that {@link isNodeNotActionableError} catches from
|
|
89
|
+
* the CDP-error side when a click is attempted anyway.
|
|
90
|
+
*/
|
|
91
|
+
const IS_VISIBLE_EXPR = `((el) => {
|
|
92
|
+
const rect = el.getBoundingClientRect();
|
|
93
|
+
if (rect.width === 0 && rect.height === 0) return false;
|
|
94
|
+
const style = getComputedStyle(el);
|
|
95
|
+
return style.display !== "none" && style.visibility !== "hidden";
|
|
96
|
+
})`;
|
|
97
|
+
/**
|
|
98
|
+
* Computes an element's accessible name so an interactive control with no
|
|
99
|
+
* visible text — an `input`/`select`/`textarea`, which the DOM spec defines
|
|
100
|
+
* as always having empty `textContent` — still yields something
|
|
101
|
+
* `scoreCandidate` (`deep-locator-candidates.ts`) and the rephrase LLM can
|
|
102
|
+
* tell apart from every other candidate. Precedence mirrors the a11y-name
|
|
103
|
+
* convention `buildRankSubmitCandidatesExpr` (`submit-control.ts`) applies at
|
|
104
|
+
* its `aria-label`-over-`textContent` tier, extended with the sources a form
|
|
105
|
+
* control actually carries: `aria-label` → `aria-labelledby` (ids resolved
|
|
106
|
+
* against `root`) → an associated `label` (`label[for=id]`, or the nearest
|
|
107
|
+
* ancestor `label`) → `placeholder` → `title` → `alt` → non-empty
|
|
108
|
+
* `textContent` → `value` (only for `input[type=button|submit]`, where
|
|
109
|
+
* `value` IS the rendered label). `<select>` skips the `textContent` tier
|
|
110
|
+
* entirely — a `<select>`'s `textContent` is every `<option>`'s text
|
|
111
|
+
* concatenated ("United StatesCanadaMexico…"), which can accidentally
|
|
112
|
+
* substring-match an unrelated step instruction and outrank the
|
|
113
|
+
* correctly-labelled field; an unlabelled `<select>` falls through to `""`
|
|
114
|
+
* (the caller's `|| ""` contract) instead. `root`-relative so the generated
|
|
115
|
+
* code never captures an outer `document` — the same contract
|
|
116
|
+
* {@link buildScanFrameCandidatesExpr} itself honors.
|
|
117
|
+
*/
|
|
118
|
+
function buildAccessibleNameExpr(root) {
|
|
119
|
+
return `((el) => {
|
|
120
|
+
const clean = (s) => {
|
|
121
|
+
const t = (s || "").trim();
|
|
122
|
+
return t.length > 0 ? t : null;
|
|
123
|
+
};
|
|
124
|
+
const attr = (name) => (el.getAttribute ? el.getAttribute(name) : null);
|
|
125
|
+
const ariaLabel = clean(attr("aria-label"));
|
|
126
|
+
if (ariaLabel) return ariaLabel;
|
|
127
|
+
const labelledBy = attr("aria-labelledby");
|
|
128
|
+
if (labelledBy) {
|
|
129
|
+
const getById = (elId) => (${root}.getElementById ? ${root}.getElementById(elId) : null);
|
|
130
|
+
const labelledText = clean(
|
|
131
|
+
labelledBy
|
|
132
|
+
.split(/\\s+/)
|
|
133
|
+
.map((elId) => {
|
|
134
|
+
const target = getById(elId);
|
|
135
|
+
return target ? target.textContent : "";
|
|
136
|
+
})
|
|
137
|
+
.join(" ")
|
|
138
|
+
);
|
|
139
|
+
if (labelledText) return labelledText;
|
|
140
|
+
}
|
|
141
|
+
const findLabelFor = (targetId) => {
|
|
142
|
+
if (!targetId || !${root}.querySelectorAll) return null;
|
|
143
|
+
const labels = Array.from(${root}.querySelectorAll("label"));
|
|
144
|
+
return (
|
|
145
|
+
labels.find((label) => label.getAttribute && label.getAttribute("for") === targetId) ||
|
|
146
|
+
null
|
|
147
|
+
);
|
|
148
|
+
};
|
|
149
|
+
const id = attr("id");
|
|
150
|
+
const associatedLabel = findLabelFor(id) || (el.closest ? el.closest("label") : null);
|
|
151
|
+
const labelText = associatedLabel ? clean(associatedLabel.textContent) : null;
|
|
152
|
+
if (labelText) return labelText;
|
|
153
|
+
const placeholder = clean(attr("placeholder"));
|
|
154
|
+
if (placeholder) return placeholder;
|
|
155
|
+
const title = clean(attr("title"));
|
|
156
|
+
if (title) return title;
|
|
157
|
+
const alt = clean(attr("alt"));
|
|
158
|
+
if (alt) return alt;
|
|
159
|
+
const tag = (el.tagName || "").toLowerCase();
|
|
160
|
+
if (tag !== "select") {
|
|
161
|
+
const text = clean(el.textContent);
|
|
162
|
+
if (text) return text;
|
|
163
|
+
}
|
|
164
|
+
const type = (attr("type") || "").toLowerCase();
|
|
165
|
+
if (tag === "input" && (type === "button" || type === "submit")) {
|
|
166
|
+
const value = clean(attr("value"));
|
|
167
|
+
if (value) return value;
|
|
168
|
+
}
|
|
169
|
+
return "";
|
|
170
|
+
})`;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Builds a self-contained evaluate expression that queries `root` for every
|
|
174
|
+
* match of `innerSelector` via `document.querySelectorAll` — the SAME
|
|
175
|
+
* resolution Stagehand's own primary CSS resolver uses
|
|
176
|
+
* (`resolveCssSelector(sel, i) = querySelectorAll(sel)[i]`, in the frame's
|
|
177
|
+
* own document) — and returns one entry per match, in document order:
|
|
178
|
+
* `{ index, text, visible }`. `index` therefore lines up 1:1 with the index
|
|
179
|
+
* `deepLocator(hop).nth(index)` will later resolve to, so a caller can scan
|
|
180
|
+
* once here and hand an index to the existing per-index click path without
|
|
181
|
+
* re-deriving the candidate set.
|
|
182
|
+
*
|
|
183
|
+
* That alignment diverges only when the light-DOM query matches fewer than
|
|
184
|
+
* `i + 1` elements, which is what triggers Stagehand's shadow-piercing
|
|
185
|
+
* `resolveCssSelectorPierce` fallback instead of the plain CSS resolver —
|
|
186
|
+
* `innerSelector` should stay a plain, non-shadow-crossing selector (e.g.
|
|
187
|
+
* {@link INTERACTIVE_CANDIDATE_SELECTOR}) whenever a caller depends on index
|
|
188
|
+
* alignment with `deepLocator`.
|
|
189
|
+
*
|
|
190
|
+
* `root` overrides the traversal root expression (default `"document"`),
|
|
191
|
+
* interpolated verbatim into the generated code so a caller evaluating this
|
|
192
|
+
* expression via `Frame.evaluate` still resolves that frame's own document —
|
|
193
|
+
* the expression never captures an outer `document` reference (mirrors
|
|
194
|
+
* `buildRankSubmitCandidatesExpr`'s contract in `submit-control.ts`).
|
|
195
|
+
*/
|
|
196
|
+
function buildScanFrameCandidatesExpr(innerSelector, root = "document") {
|
|
197
|
+
return `(() => {
|
|
198
|
+
const isVisible = ${IS_VISIBLE_EXPR};
|
|
199
|
+
const accessibleName = ${buildAccessibleNameExpr(root)};
|
|
200
|
+
const matches = Array.from(${root}.querySelectorAll(${JSON.stringify(innerSelector)}));
|
|
201
|
+
return matches.map((el, index) => ({
|
|
202
|
+
index,
|
|
203
|
+
text: accessibleName(el),
|
|
204
|
+
visible: isVisible(el),
|
|
205
|
+
}));
|
|
206
|
+
})()`;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Builds a self-contained evaluate expression that re-runs the SAME
|
|
210
|
+
* `root.querySelectorAll(innerSelector)` resolution {@link buildScanFrameCandidatesExpr}
|
|
211
|
+
* uses and clicks the element at `index` (dispatching focus + bubbling
|
|
212
|
+
* mousedown/mouseup/click, matching `buildClickByDeepIndexExpr`'s
|
|
213
|
+
* (`submit-control.ts`) controlled-state click convention). This is the
|
|
214
|
+
* one-round-trip actuation half of the batched-scan fix: a caller that
|
|
215
|
+
* scanned via {@link buildScanFrameCandidatesExpr} can hand the chosen
|
|
216
|
+
* `index` straight to this builder without re-deriving the candidate set,
|
|
217
|
+
* because both builders resolve `querySelectorAll(innerSelector)` against
|
|
218
|
+
* the same frame document in the same match order.
|
|
219
|
+
*
|
|
220
|
+
* Reports out-of-range / not-actionable outcomes as data rather than by
|
|
221
|
+
* throwing, mirroring {@link isNodeNotActionableError}'s CDP-error-side
|
|
222
|
+
* contract from the DOM-observable side:
|
|
223
|
+
* - `index` no longer matches (e.g. the DOM changed between scan and click)
|
|
224
|
+
* returns `{ clicked: false, reason: "out-of-range" }`.
|
|
225
|
+
* - the matched element fails {@link IS_VISIBLE_EXPR} — the same
|
|
226
|
+
* visibility check {@link buildScanFrameCandidatesExpr} reports as
|
|
227
|
+
* `visible: false` — returns `{ clicked: false, reason: "not-actionable" }`
|
|
228
|
+
* instead of dispatching a click a real browser would reject with a
|
|
229
|
+
* `-32000 Node does not have a layout object` CDP error.
|
|
230
|
+
*
|
|
231
|
+
* `root` overrides the traversal root expression (default `"document"`) and
|
|
232
|
+
* must match the `root` passed to the {@link buildScanFrameCandidatesExpr}
|
|
233
|
+
* call that produced `index`, or the re-run query will not resolve against
|
|
234
|
+
* the same document. Interpolated verbatim so a caller evaluating this
|
|
235
|
+
* expression via `Frame.evaluate` still resolves that frame's own document —
|
|
236
|
+
* the expression never captures an outer `document` reference.
|
|
237
|
+
*/
|
|
238
|
+
function buildClickFrameCandidateExpr(innerSelector, index, root = "document") {
|
|
239
|
+
return `(() => {
|
|
240
|
+
const isVisible = ${IS_VISIBLE_EXPR};
|
|
241
|
+
const matches = Array.from(${root}.querySelectorAll(${JSON.stringify(innerSelector)}));
|
|
242
|
+
const el = matches[${JSON.stringify(index)}];
|
|
243
|
+
if (!el) return { clicked: false, reason: "out-of-range" };
|
|
244
|
+
if (!isVisible(el)) return { clicked: false, reason: "not-actionable" };
|
|
245
|
+
if (typeof el.focus === "function") { try { el.focus(); } catch (e) {} }
|
|
246
|
+
el.dispatchEvent(new Event("mousedown", { bubbles: true }));
|
|
247
|
+
el.dispatchEvent(new Event("mouseup", { bubbles: true }));
|
|
248
|
+
el.dispatchEvent(new Event("click", { bubbles: true }));
|
|
249
|
+
return { clicked: true };
|
|
250
|
+
})()`;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Builds the write body {@link buildFillFrameCandidateExpr} and
|
|
254
|
+
* {@link buildSelectFrameCandidateExpr} both interpolate: re-runs the SAME
|
|
255
|
+
* `root.querySelectorAll(innerSelector)` resolution
|
|
256
|
+
* {@link buildScanFrameCandidatesExpr}/{@link buildClickFrameCandidateExpr}
|
|
257
|
+
* use, guards out-of-range/not-actionable the same way as
|
|
258
|
+
* {@link buildClickFrameCandidateExpr}, then writes `value` through
|
|
259
|
+
* `nativePrototypeExpr`'s `value` setter descriptor rather than a bare
|
|
260
|
+
* `el.value = value` assignment — a React/Angular/Vue controlled component
|
|
261
|
+
* shadows the setter at the instance level, so a bare assignment can be
|
|
262
|
+
* silently absorbed by the framework's own value tracking while the DOM
|
|
263
|
+
* read-back still looks correct (a silent false positive); calling the
|
|
264
|
+
* descriptor's setter explicitly restores native behavior, mirroring
|
|
265
|
+
* `fillHtml5DateTimeInput`'s and `applySelectValue`'s identical workaround in
|
|
266
|
+
* `flow-runner.ts`. Falls back to a bare assignment when no such descriptor
|
|
267
|
+
* exists (a plain, unmanaged form control). Dispatches bubbling `input` then
|
|
268
|
+
* `change` then `blur` — the sequence a controlled component's `onChange`/
|
|
269
|
+
* `onBlur` listen for — before reading `el.value` back into the returned
|
|
270
|
+
* payload, so the caller gets write + verify in one round-trip instead of a
|
|
271
|
+
* second `inputValue()` call.
|
|
272
|
+
*
|
|
273
|
+
* `nativePrototypeExpr` is interpolated verbatim as a JS expression
|
|
274
|
+
* evaluated inside the generated code (with `el` in scope) rather than
|
|
275
|
+
* passed as a resolved value, so {@link buildFillFrameCandidateExpr} can
|
|
276
|
+
* choose between `HTMLInputElement`/`HTMLTextAreaElement` based on the
|
|
277
|
+
* resolved element's own tag at evaluate-time.
|
|
278
|
+
*/
|
|
279
|
+
function buildWriteFrameCandidateExpr(innerSelector, index, value, nativePrototypeExpr, root) {
|
|
280
|
+
return `(() => {
|
|
281
|
+
const isVisible = ${IS_VISIBLE_EXPR};
|
|
282
|
+
const matches = Array.from(${root}.querySelectorAll(${JSON.stringify(innerSelector)}));
|
|
283
|
+
const el = matches[${JSON.stringify(index)}];
|
|
284
|
+
if (!el) return { written: false, reason: "out-of-range" };
|
|
285
|
+
if (!isVisible(el)) return { written: false, reason: "not-actionable" };
|
|
286
|
+
const value = ${JSON.stringify(value)};
|
|
287
|
+
const descriptor = Object.getOwnPropertyDescriptor(${nativePrototypeExpr}, "value");
|
|
288
|
+
if (descriptor && descriptor.set) { descriptor.set.call(el, value); } else { el.value = value; }
|
|
289
|
+
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
290
|
+
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
291
|
+
el.dispatchEvent(new Event("blur", { bubbles: true }));
|
|
292
|
+
return { written: true, readBack: el.value };
|
|
293
|
+
})()`;
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Builds a self-contained evaluate expression that fills the `<input>`/
|
|
297
|
+
* `<textarea>` at `root.querySelectorAll(innerSelector)[index]` with
|
|
298
|
+
* `value` — the one-round-trip write half of
|
|
299
|
+
* {@link fillDeepLocatorCandidate}'s (`deep-locator-actuate.ts`) contract.
|
|
300
|
+
* See {@link buildWriteFrameCandidateExpr} for the shared write/dispatch/
|
|
301
|
+
* read-back mechanism and out-of-range/not-actionable reporting. Resolves
|
|
302
|
+
* the native value-setter descriptor off `HTMLTextAreaElement.prototype`
|
|
303
|
+
* when the matched element's own `tagName` is `"textarea"`, else
|
|
304
|
+
* `HTMLInputElement.prototype` — determined at evaluate-time (inside the
|
|
305
|
+
* generated code), since `innerSelector` may match a mix of input and
|
|
306
|
+
* textarea nodes (e.g. {@link INTERACTIVE_CANDIDATE_SELECTOR}).
|
|
307
|
+
*
|
|
308
|
+
* `root` overrides the traversal root expression (default `"document"`) and
|
|
309
|
+
* must match the `root` a prior {@link buildScanFrameCandidatesExpr} call
|
|
310
|
+
* used to derive `index`, mirroring
|
|
311
|
+
* {@link buildClickFrameCandidateExpr}'s `root` contract.
|
|
312
|
+
*/
|
|
313
|
+
function buildFillFrameCandidateExpr(innerSelector, index, value, root = "document") {
|
|
314
|
+
const nativePrototypeExpr = '(el.tagName && el.tagName.toLowerCase() === "textarea" ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype)';
|
|
315
|
+
return buildWriteFrameCandidateExpr(innerSelector, index, value, nativePrototypeExpr, root);
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Builds a self-contained evaluate expression that re-runs the SAME
|
|
319
|
+
* `root.querySelectorAll(innerSelector)` resolution and selects, on the
|
|
320
|
+
* `<select>`-shaped candidate at `index`, the option whose `value` — or,
|
|
321
|
+
* failing that, whose trimmed visible label — matches `value`. Matching by
|
|
322
|
+
* value first mirrors what a `<select>`'s own `value` property actually is
|
|
323
|
+
* (the matched option's `value` attribute); falling back to the trimmed
|
|
324
|
+
* label covers a flow instruction that quotes the option's VISIBLE label
|
|
325
|
+
* instead (`parseSelectStep`), which a `<select>`'s DOM `value` never is —
|
|
326
|
+
* the same value-then-label tolerance `trySelectPrimitive`'s deterministic
|
|
327
|
+
* match already applies across every `<select>` on the page
|
|
328
|
+
* (`flow-runner.ts:3576`), reproduced here inline because this expression
|
|
329
|
+
* matches options within one already-chosen candidate rather than
|
|
330
|
+
* enumerating the whole page. See {@link buildWriteFrameCandidateExpr} for
|
|
331
|
+
* the write/dispatch/read-back mechanism this reimplements to accommodate
|
|
332
|
+
* the option lookup — the shared helper alone can't express "write the
|
|
333
|
+
* MATCHED option's value, not the caller's raw string."
|
|
334
|
+
*
|
|
335
|
+
* `root` overrides the traversal root expression (default `"document"`) and
|
|
336
|
+
* must match the `root` a prior {@link buildScanFrameCandidatesExpr} call
|
|
337
|
+
* used to derive `index`, mirroring
|
|
338
|
+
* {@link buildClickFrameCandidateExpr}'s `root` contract. A candidate with
|
|
339
|
+
* no option matching `value` by either value or label reports
|
|
340
|
+
* `{ written: false, reason: "not-actionable" }` — nothing on the page can
|
|
341
|
+
* satisfy the write.
|
|
342
|
+
*/
|
|
343
|
+
function buildSelectFrameCandidateExpr(innerSelector, index, value, root = "document") {
|
|
344
|
+
return `(() => {
|
|
345
|
+
const isVisible = ${IS_VISIBLE_EXPR};
|
|
346
|
+
const matches = Array.from(${root}.querySelectorAll(${JSON.stringify(innerSelector)}));
|
|
347
|
+
const el = matches[${JSON.stringify(index)}];
|
|
348
|
+
if (!el) return { written: false, reason: "out-of-range" };
|
|
349
|
+
if (!isVisible(el)) return { written: false, reason: "not-actionable" };
|
|
350
|
+
const wanted = ${JSON.stringify(value)};
|
|
351
|
+
const wantedTrimmed = wanted.trim();
|
|
352
|
+
const options = Array.from(el.options || []);
|
|
353
|
+
const target =
|
|
354
|
+
options.find((o) => o.value === wanted) ||
|
|
355
|
+
options.find((o) => (o.textContent || "").trim() === wantedTrimmed);
|
|
356
|
+
if (!target) return { written: false, reason: "not-actionable" };
|
|
357
|
+
const descriptor = Object.getOwnPropertyDescriptor(HTMLSelectElement.prototype, "value");
|
|
358
|
+
if (descriptor && descriptor.set) { descriptor.set.call(el, target.value); } else { el.value = target.value; }
|
|
359
|
+
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
360
|
+
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
361
|
+
el.dispatchEvent(new Event("blur", { bubbles: true }));
|
|
362
|
+
return { written: true, readBack: el.value };
|
|
363
|
+
})()`;
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Detects the CDP "node is not actionable" failure shape so a caller
|
|
367
|
+
* cascading through candidates can treat it as "skip this candidate, try the
|
|
368
|
+
* next" instead of scoring the attempt as a failed click. Covers both
|
|
369
|
+
* surfaces a click against an unrendered node can throw through: a raw CDP
|
|
370
|
+
* protocol error rejects as `new Error(`${code} ${message}`)`
|
|
371
|
+
* (understudy/cdp.js), so a `DOM.getBoxModel`/`DOM.scrollIntoViewIfNeeded`
|
|
372
|
+
* failure over "no layout object" arrives as a plain `Error` whose message is
|
|
373
|
+
* exactly `-32000 Node does not have a layout object`; separately,
|
|
374
|
+
* `Locator.click()`/`Locator.hover()` (understudy/locator.js) throw
|
|
375
|
+
* `ElementNotVisibleError` directly when `DOM.getBoxModel` returns no model.
|
|
376
|
+
* A name/substring check on both shapes is sufficient — no new error class
|
|
377
|
+
* is needed.
|
|
378
|
+
*/
|
|
379
|
+
function isNodeNotActionableError(error) {
|
|
380
|
+
if (!(error instanceof Error))
|
|
381
|
+
return false;
|
|
382
|
+
if (error.name === "ElementNotVisibleError")
|
|
383
|
+
return true;
|
|
384
|
+
return /node does not have a layout object/i.test(error.message);
|
|
385
|
+
}
|
|
386
|
+
//# sourceMappingURL=deep-locator-scan.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deep-locator-scan.js","sourceRoot":"","sources":["../../src/scraper/deep-locator-scan.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;;;;;;;;AAEH;;;;;;;;GAQG;AACU,QAAA,8BAA8B,GACzC,+DAA+D,CAAC;AAElE;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACU,QAAA,sCAAsC,GAAG,KAAK,CAAC;AAE5D;;;;;;GAMG;AACH,MAAM,eAAe,GAAG;;;;;GAKrB,CAAC;AAEJ;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,SAAS,uBAAuB,CAAC,IAAY;IAC3C,OAAO;;;;;;;;;;mCAU0B,IAAI,qBAAqB,IAAI;;;;;;;;;;;;;0BAatC,IAAI;kCACI,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;KA2BjC,CAAC;AACN,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,sCAA6C,aAAqB,EAAE,IAAI,GAAG,UAAU;IACnF,OAAO;wBACe,eAAe;6BACV,uBAAuB,CAAC,IAAI,CAAC;iCACzB,IAAI,qBAAqB,IAAI,CAAC,SAAS,CAAC,aAAa,CAAC;;;;;;OAMhF,CAAC;AACR,CAAC;AAYD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,sCACE,aAAqB,EACrB,KAAa,EACb,IAAI,GAAG,UAAU;IAEjB,OAAO;wBACe,eAAe;iCACN,IAAI,qBAAqB,IAAI,CAAC,SAAS,CAAC,aAAa,CAAC;yBAC9D,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC;;;;;;;;OAQvC,CAAC;AACR,CAAC;AAwBD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,SAAS,4BAA4B,CACnC,aAAqB,EACrB,KAAa,EACb,KAAa,EACb,mBAA2B,EAC3B,IAAY;IAEZ,OAAO;wBACe,eAAe;iCACN,IAAI,qBAAqB,IAAI,CAAC,SAAS,CAAC,aAAa,CAAC;yBAC9D,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC;;;oBAG1B,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC;yDACgB,mBAAmB;;;;;;OAMrE,CAAC;AACR,CAAC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,qCACE,aAAqB,EACrB,KAAa,EACb,KAAa,EACb,IAAI,GAAG,UAAU;IAEjB,MAAM,mBAAmB,GACvB,sHAAsH,CAAC;IACzH,OAAO,4BAA4B,CAAC,aAAa,EAAE,KAAK,EAAE,KAAK,EAAE,mBAAmB,EAAE,IAAI,CAAC,CAAC;AAC9F,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,uCACE,aAAqB,EACrB,KAAa,EACb,KAAa,EACb,IAAI,GAAG,UAAU;IAEjB,OAAO;wBACe,eAAe;iCACN,IAAI,qBAAqB,IAAI,CAAC,SAAS,CAAC,aAAa,CAAC;yBAC9D,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC;;;qBAGzB,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC;;;;;;;;;;;;;OAanC,CAAC;AACR,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,kCAAyC,KAAc;IACrD,IAAI,CAAC,CAAC,KAAK,YAAY,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IAC5C,IAAI,KAAK,CAAC,IAAI,KAAK,wBAAwB;QAAE,OAAO,IAAI,CAAC;IACzD,OAAO,qCAAqC,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;AACnE,CAAC"}
|