@jobshimo/browser-link 0.23.6 → 0.23.8
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/agent-instructions/content.js +17 -7
- package/dist/agent-instructions/content.js.map +1 -1
- package/dist/cdp/flow.d.ts +74 -10
- package/dist/cdp/flow.js +56 -19
- package/dist/cdp/flow.js.map +1 -1
- package/dist/cdp/inpage/builders.d.ts +17 -2
- package/dist/cdp/inpage/builders.js +13 -8
- package/dist/cdp/inpage/builders.js.map +1 -1
- package/dist/cdp/inpage/deep-query.d.ts +1 -1
- package/dist/cdp/inpage/deep-query.js +27 -1
- package/dist/cdp/inpage/deep-query.js.map +1 -1
- package/dist/cdp/inpage/dom-helpers.d.ts +1 -1
- package/dist/cdp/inpage/dom-helpers.js +16 -0
- package/dist/cdp/inpage/dom-helpers.js.map +1 -1
- package/dist/cdp/transport.js +7 -0
- package/dist/cdp/transport.js.map +1 -1
- package/dist/commands/about.js +2 -2
- package/dist/commands/about.js.map +1 -1
- package/dist/extension/background.js +297 -276
- package/dist/extension/background.js.map +1 -1
- package/dist/extension/flow.d.ts +74 -10
- package/dist/extension/flow.js +56 -19
- package/dist/extension/flow.js.map +1 -1
- package/dist/extension/inpage/builders.d.ts +17 -2
- package/dist/extension/inpage/builders.js +13 -8
- package/dist/extension/inpage/builders.js.map +1 -1
- package/dist/extension/inpage/deep-query.d.ts +1 -1
- package/dist/extension/inpage/deep-query.js +27 -1
- package/dist/extension/inpage/deep-query.js.map +1 -1
- package/dist/extension/inpage/dom-helpers.d.ts +1 -1
- package/dist/extension/inpage/dom-helpers.js +16 -0
- package/dist/extension/inpage/dom-helpers.js.map +1 -1
- package/dist/extension/manifest.json +1 -1
- package/dist/map/tools.js +2 -2
- package/dist/map/tools.js.map +1 -1
- package/dist/permissions.js +1 -1
- package/dist/permissions.js.map +1 -1
- package/dist/tools/browser-definitions.js +36 -8
- package/dist/tools/browser-definitions.js.map +1 -1
- package/dist/tools/browser-dispatch.js +62 -2
- package/dist/tools/browser-dispatch.js.map +1 -1
- package/dist/tools/server-instructions.js +10 -4
- package/dist/tools/server-instructions.js.map +1 -1
- package/package.json +1 -1
|
@@ -76,9 +76,10 @@ function body() {
|
|
|
76
76
|
'',
|
|
77
77
|
'A `list_tabs` entry has `tab_id` starting with `cdp:` (`transport: "cdp"`):',
|
|
78
78
|
'→ that tab is reached via cdp-direct, not the extension — every tool',
|
|
79
|
-
'works the same EXCEPT drag/console/
|
|
80
|
-
'canvas_screenshot/dialog_respond/set_permission/
|
|
81
|
-
'error naming the extension as the fallback).
|
|
79
|
+
'works the same EXCEPT drag (standalone AND as a flow step)/console/',
|
|
80
|
+
'network/network_body/canvas_screenshot/dialog_respond/set_permission/',
|
|
81
|
+
'wait_for_tab (clear error naming the extension as the fallback).',
|
|
82
|
+
'cdp-direct only appears',
|
|
82
83
|
'when the user has both enabled it AND granted access — you cannot',
|
|
83
84
|
'enable or grant it yourself. A "cdp-direct is disabled" or "requires an',
|
|
84
85
|
'active grant" error means relay the suggested command to the user',
|
|
@@ -112,9 +113,14 @@ function body() {
|
|
|
112
113
|
' costs ONE `browser.flow` call (two if a step fails and you need a',
|
|
113
114
|
" corrective follow-up). A `find` step's selector becomes the implicit",
|
|
114
115
|
' target for the very next click/type/press step that omits',
|
|
115
|
-
' `selector`.
|
|
116
|
-
'
|
|
117
|
-
'
|
|
116
|
+
' `selector`. Repeated drag-and-drop (card-matching exercises, sortable',
|
|
117
|
+
' lists) batches the same way: N `drag` steps — same params as',
|
|
118
|
+
' browser.drag, endpoints always explicit, drag stays OUT of the',
|
|
119
|
+
' implicit-target chain — in ONE flow instead of N browser.drag calls',
|
|
120
|
+
' (extension tabs only; a drag step on a cdp: tab fails at that step).',
|
|
121
|
+
' Flow is strictly sequential and fails fast — reach for individual',
|
|
122
|
+
' click/type/press/find/wait_for/drag calls only when you need to',
|
|
123
|
+
' branch on an intermediate result.',
|
|
118
124
|
'- Finding an element by visible text → `browser.find({ text, role? })`,',
|
|
119
125
|
' NOT `browser.evaluate` with a `textContent` grep. `find` covers',
|
|
120
126
|
' `<div onclick>` (which a naive `querySelectorAll("button")` MISSES',
|
|
@@ -135,7 +141,11 @@ function body() {
|
|
|
135
141
|
' cross-origin iframes stay out of reach (no CDP workaround). If',
|
|
136
142
|
' `click` returns "Element covered by …", something else is on top of',
|
|
137
143
|
' the target — click or dismiss that element first, do not blindly',
|
|
138
|
-
' retry with `force:true`.',
|
|
144
|
+
' retry with `force:true`. A pointer-events:none target (invisible',
|
|
145
|
+
' a11y layer) is NOT reported as covered: the click proceeds and the',
|
|
146
|
+
' ok result carries `hit_element` naming what actually received it —',
|
|
147
|
+
' present on any off-target click (force:true included), omitted',
|
|
148
|
+
' when the click hit the resolved element itself.',
|
|
139
149
|
'- A snapshot/find entry with `ambiguous: true` has a selector that',
|
|
140
150
|
' matches structurally-identical twins in other shadow roots/iframes',
|
|
141
151
|
' (no CSS syntax can scope to one root). It resolves first-match-wins:',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"content.js","sourceRoot":"","sources":["../../src/agent-instructions/content.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAExC;;;;;;;;;;GAUG;AAEH,MAAM,CAAC,MAAM,YAAY,GAAG,sCAAsC,CAAC;AACnE,MAAM,CAAC,MAAM,UAAU,GAAG,wCAAwC,CAAC;AAEnE,MAAM,UAAU,WAAW,CAAC,OAAO,GAAG,OAAO;IAC3C,OAAO,GAAG,YAAY,KAAK,OAAO,MAAM,CAAC;AAC3C,CAAC;AAED;;;;yCAIyC;AACzC,MAAM,CAAC,MAAM,QAAQ,GACnB,gFAAgF,CAAC;AACnF,MAAM,CAAC,MAAM,MAAM,GAAG,4CAA4C,CAAC;AAEnE;;;;mBAImB;AACnB,SAAS,IAAI;IACX,OAAO;QACL,mDAAmD;QACnD,EAAE;QACF,kFAAkF;QAClF,wEAAwE;QACxE,+DAA+D;QAC/D,EAAE;QACF,0DAA0D;QAC1D,EAAE;QACF,kDAAkD;QAClD,qEAAqE;QACrE,+DAA+D;QAC/D,yEAAyE;QACzE,oDAAoD;QACpD,yEAAyE;QACzE,sEAAsE;QACtE,kFAAkF;QAClF,EAAE;QACF,iDAAiD;QACjD,kCAAkC;QAClC,mEAAmE;QACnE,sEAAsE;QACtE,qEAAqE;QACrE,EAAE;QACF,qDAAqD;QACrD,qEAAqE;QACrE,EAAE;QACF,yCAAyC;QACzC,gEAAgE;QAChE,EAAE;QACF,yEAAyE;QACzE,mBAAmB;QACnB,qEAAqE;QACrE,wEAAwE;QACxE,qDAAqD;QACrD,EAAE;QACF,2DAA2D;QAC3D,iEAAiE;QACjE,sEAAsE;QACtE,qEAAqE;QACrE,0CAA0C;QAC1C,EAAE;QACF,4DAA4D;QAC5D,wEAAwE;QACxE,0CAA0C;QAC1C,EAAE;QACF,6EAA6E;QAC7E,sEAAsE;QACtE,
|
|
1
|
+
{"version":3,"file":"content.js","sourceRoot":"","sources":["../../src/agent-instructions/content.ts"],"names":[],"mappings":"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"}
|
package/dist/cdp/flow.d.ts
CHANGED
|
@@ -11,10 +11,14 @@
|
|
|
11
11
|
* sibling at `packages/extension/src/flow.ts`.
|
|
12
12
|
*
|
|
13
13
|
* browser.flow step sequencing — the logic behind the composite action
|
|
14
|
-
* tool. Runs a short, declarative list of find/click/type/press/wait_for
|
|
15
|
-
* steps strictly in order, fails fast on the first step that does
|
|
16
|
-
* succeed, and threads an "implicit target" from a `find` step into
|
|
17
|
-
* very next click/type/press step that omits its own `selector`.
|
|
14
|
+
* tool. Runs a short, declarative list of find/click/type/press/wait_for/
|
|
15
|
+
* drag steps strictly in order, fails fast on the first step that does
|
|
16
|
+
* not succeed, and threads an "implicit target" from a `find` step into
|
|
17
|
+
* the very next click/type/press step that omits its own `selector`. A
|
|
18
|
+
* `drag` step stays OUT of that chain: both of its endpoints are always
|
|
19
|
+
* explicit (selector or coords), so it neither consumes nor sets the
|
|
20
|
+
* implicit target — a pending target survives a drag step untouched for
|
|
21
|
+
* a later click/type/press, exactly like it survives a wait_for.
|
|
18
22
|
*
|
|
19
23
|
* Deliberately decoupled from any specific transport: every side effect
|
|
20
24
|
* goes through the `FlowDeps` the caller injects. The concrete wiring —
|
|
@@ -24,12 +28,16 @@
|
|
|
24
28
|
* step-shape validation, implicit-target threading, fail-fast, result
|
|
25
29
|
* shaping — unit-testable without a real browser tab or CDP session.
|
|
26
30
|
*
|
|
27
|
-
* The individual `click` / `type` / `press` / `find` / `wait_for`
|
|
28
|
-
* handlers (`background.ts`'s `handleTool` switch in the
|
|
29
|
-
* `callCdpTool`'s switch in `cdp/transport.ts` on the server)
|
|
30
|
-
* module's step executor call the exact SAME perform* functions
|
|
31
|
-
* is one implementation of each action, never two copies that
|
|
32
|
-
* apart.
|
|
31
|
+
* The individual `click` / `type` / `press` / `find` / `wait_for` /
|
|
32
|
+
* `drag` tool handlers (`background.ts`'s `handleTool` switch in the
|
|
33
|
+
* extension, `callCdpTool`'s switch in `cdp/transport.ts` on the server)
|
|
34
|
+
* and this module's step executor call the exact SAME perform* functions
|
|
35
|
+
* — there is one implementation of each action, never two copies that
|
|
36
|
+
* can drift apart. The one deliberate asymmetry is `drag` over
|
|
37
|
+
* cdp-direct: the tool is out of cdp-direct's v1 scope (see the server's
|
|
38
|
+
* `cdp/support.ts`), so the server wires a `performDrag` that always
|
|
39
|
+
* fails with the same unsupported-transport error the standalone
|
|
40
|
+
* `browser.drag` returns on a `cdp:` tab.
|
|
33
41
|
*/
|
|
34
42
|
/** Structured success/failure outcome shared by every perform* function
|
|
35
43
|
* (`background.ts` in the extension, `cdp/transport.ts` on the server).
|
|
@@ -94,6 +102,13 @@ export interface ClickStepParams {
|
|
|
94
102
|
export interface ClickStepResult {
|
|
95
103
|
clicked: string;
|
|
96
104
|
tag: string;
|
|
105
|
+
/** Descriptor of the element that actually receives the dispatched
|
|
106
|
+
* click, present ONLY when it differs from the resolved target: a
|
|
107
|
+
* pointer-events:none target whose real hit-target sibling takes the
|
|
108
|
+
* click, an ancestor wrapper, or a `force:true` click landing on
|
|
109
|
+
* whatever covers the point (see `buildClickResolveJs`). Omitted when
|
|
110
|
+
* the click lands on the target itself. */
|
|
111
|
+
hit_element?: string;
|
|
97
112
|
settle?: Record<string, unknown>;
|
|
98
113
|
}
|
|
99
114
|
export interface TypeStepParams {
|
|
@@ -132,6 +147,46 @@ export interface WaitForStepResult {
|
|
|
132
147
|
checks: number;
|
|
133
148
|
reason?: string;
|
|
134
149
|
}
|
|
150
|
+
/** Mirrors the standalone `browser.drag` tool's params exactly (minus
|
|
151
|
+
* `tab_id`): each endpoint is a CSS selector OR a viewport coordinate
|
|
152
|
+
* pair, plus the same interpolation/hold options. Endpoint validation
|
|
153
|
+
* (missing both forms, offscreen points) lives in `performDrag` itself,
|
|
154
|
+
* so a flow drag step fails with the exact same error strings the
|
|
155
|
+
* standalone tool returns. */
|
|
156
|
+
export interface DragStepParams {
|
|
157
|
+
from_selector?: string;
|
|
158
|
+
from_x?: number;
|
|
159
|
+
from_y?: number;
|
|
160
|
+
to_selector?: string;
|
|
161
|
+
to_x?: number;
|
|
162
|
+
to_y?: number;
|
|
163
|
+
duration_ms?: number;
|
|
164
|
+
hold_before_move_ms?: number;
|
|
165
|
+
hold_before_release_ms?: number;
|
|
166
|
+
}
|
|
167
|
+
/** Full result of one drag — the same shape the standalone tool returns.
|
|
168
|
+
* A flow step's per-step entry keeps only `drag_mode` of this (see
|
|
169
|
+
* `compactActionResult`): the endpoints and durations echo what the
|
|
170
|
+
* caller asked for, while `drag_mode` is NEW information — whether HTML5
|
|
171
|
+
* native drag (dragstart/drop) or pointer-only events fired, the signal
|
|
172
|
+
* for diagnosing a silently-failing drop. */
|
|
173
|
+
export interface DragStepResult {
|
|
174
|
+
from: {
|
|
175
|
+
x: number;
|
|
176
|
+
y: number;
|
|
177
|
+
selector: string | null;
|
|
178
|
+
};
|
|
179
|
+
to: {
|
|
180
|
+
x: number;
|
|
181
|
+
y: number;
|
|
182
|
+
selector: string | null;
|
|
183
|
+
};
|
|
184
|
+
duration_ms_actual: number;
|
|
185
|
+
drag_mode: 'html5' | 'pointer';
|
|
186
|
+
interception_attempted: boolean;
|
|
187
|
+
intercept_received: boolean;
|
|
188
|
+
events_fired: string[];
|
|
189
|
+
}
|
|
135
190
|
export type FlowStep = {
|
|
136
191
|
find: FindStepParams;
|
|
137
192
|
} | {
|
|
@@ -142,6 +197,8 @@ export type FlowStep = {
|
|
|
142
197
|
press: PressStepParams;
|
|
143
198
|
} | {
|
|
144
199
|
wait_for: WaitForStepParams;
|
|
200
|
+
} | {
|
|
201
|
+
drag: DragStepParams;
|
|
145
202
|
};
|
|
146
203
|
/** Everything the step executor needs from the outside world. `background.ts`
|
|
147
204
|
* (extension) / `cdp/transport.ts` (server) binds each method to the real
|
|
@@ -159,6 +216,13 @@ export interface FlowDeps {
|
|
|
159
216
|
performType(params: TypeStepParams): Promise<ActionOutcome<TypeStepResult>>;
|
|
160
217
|
performPress(params: PressStepParams): Promise<ActionOutcome<PressStepResult>>;
|
|
161
218
|
performWaitFor(params: WaitForStepParams): Promise<ActionOutcome<WaitForStepResult>>;
|
|
219
|
+
/** The extension binds the real drag implementation; the server's
|
|
220
|
+
* cdp-direct wiring binds a stub that always returns the
|
|
221
|
+
* unsupported-transport `ok:false` (drag is out of cdp-direct's v1
|
|
222
|
+
* scope — see the server's `cdp/support.ts`), so a flow drag step on a
|
|
223
|
+
* `cdp:` tab fails fast with the exact error the standalone
|
|
224
|
+
* `browser.drag` returns there. */
|
|
225
|
+
performDrag(params: DragStepParams): Promise<ActionOutcome<DragStepResult>>;
|
|
162
226
|
buildRecoverySnapshot(): Promise<unknown>;
|
|
163
227
|
}
|
|
164
228
|
export interface FlowSuccess {
|
package/dist/cdp/flow.js
CHANGED
|
@@ -11,10 +11,14 @@
|
|
|
11
11
|
* sibling at `packages/extension/src/flow.ts`.
|
|
12
12
|
*
|
|
13
13
|
* browser.flow step sequencing — the logic behind the composite action
|
|
14
|
-
* tool. Runs a short, declarative list of find/click/type/press/wait_for
|
|
15
|
-
* steps strictly in order, fails fast on the first step that does
|
|
16
|
-
* succeed, and threads an "implicit target" from a `find` step into
|
|
17
|
-
* very next click/type/press step that omits its own `selector`.
|
|
14
|
+
* tool. Runs a short, declarative list of find/click/type/press/wait_for/
|
|
15
|
+
* drag steps strictly in order, fails fast on the first step that does
|
|
16
|
+
* not succeed, and threads an "implicit target" from a `find` step into
|
|
17
|
+
* the very next click/type/press step that omits its own `selector`. A
|
|
18
|
+
* `drag` step stays OUT of that chain: both of its endpoints are always
|
|
19
|
+
* explicit (selector or coords), so it neither consumes nor sets the
|
|
20
|
+
* implicit target — a pending target survives a drag step untouched for
|
|
21
|
+
* a later click/type/press, exactly like it survives a wait_for.
|
|
18
22
|
*
|
|
19
23
|
* Deliberately decoupled from any specific transport: every side effect
|
|
20
24
|
* goes through the `FlowDeps` the caller injects. The concrete wiring —
|
|
@@ -24,12 +28,16 @@
|
|
|
24
28
|
* step-shape validation, implicit-target threading, fail-fast, result
|
|
25
29
|
* shaping — unit-testable without a real browser tab or CDP session.
|
|
26
30
|
*
|
|
27
|
-
* The individual `click` / `type` / `press` / `find` / `wait_for`
|
|
28
|
-
* handlers (`background.ts`'s `handleTool` switch in the
|
|
29
|
-
* `callCdpTool`'s switch in `cdp/transport.ts` on the server)
|
|
30
|
-
* module's step executor call the exact SAME perform* functions
|
|
31
|
-
* is one implementation of each action, never two copies that
|
|
32
|
-
* apart.
|
|
31
|
+
* The individual `click` / `type` / `press` / `find` / `wait_for` /
|
|
32
|
+
* `drag` tool handlers (`background.ts`'s `handleTool` switch in the
|
|
33
|
+
* extension, `callCdpTool`'s switch in `cdp/transport.ts` on the server)
|
|
34
|
+
* and this module's step executor call the exact SAME perform* functions
|
|
35
|
+
* — there is one implementation of each action, never two copies that
|
|
36
|
+
* can drift apart. The one deliberate asymmetry is `drag` over
|
|
37
|
+
* cdp-direct: the tool is out of cdp-direct's v1 scope (see the server's
|
|
38
|
+
* `cdp/support.ts`), so the server wires a `performDrag` that always
|
|
39
|
+
* fails with the same unsupported-transport error the standalone
|
|
40
|
+
* `browser.drag` returns on a `cdp:` tab.
|
|
33
41
|
*/
|
|
34
42
|
/** Hard cap on steps per flow — mirrors the `maxItems: 20` on the MCP
|
|
35
43
|
* schema (`browser-definitions.ts`) and the server-side validation
|
|
@@ -37,11 +45,11 @@
|
|
|
37
45
|
* nor cdp-direct ever runs an oversized flow even if a future caller
|
|
38
46
|
* bypasses the server's own check (e.g. a directly-wired test harness). */
|
|
39
47
|
export const MAX_FLOW_STEPS = 20;
|
|
40
|
-
const STEP_KINDS = ['find', 'click', 'type', 'press', 'wait_for'];
|
|
48
|
+
const STEP_KINDS = ['find', 'click', 'type', 'press', 'wait_for', 'drag'];
|
|
41
49
|
function isStepKind(key) {
|
|
42
50
|
return STEP_KINDS.includes(key);
|
|
43
51
|
}
|
|
44
|
-
/** Identify which of the
|
|
52
|
+
/** Identify which of the 6 step kinds a raw (untrusted) step object is.
|
|
45
53
|
* Returns `null` unless the object has EXACTLY ONE key and that key is a
|
|
46
54
|
* recognized kind — extra keys (`{find: {...}, mystery: 1}`) reject, in
|
|
47
55
|
* line with the MCP schema's `additionalProperties: false`. The caller
|
|
@@ -54,13 +62,23 @@ function stepKind(step) {
|
|
|
54
62
|
return null;
|
|
55
63
|
return isStepKind(keys[0]) ? keys[0] : null;
|
|
56
64
|
}
|
|
57
|
-
/** Drop the noisy fields of a click/type/press result down to
|
|
58
|
-
* for the flow's per-step
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
65
|
+
/** Drop the noisy fields of a click/type/press/drag result down to
|
|
66
|
+
* `{ ok, settle?, hit_element?, drag_mode? }` for the flow's per-step
|
|
67
|
+
* result — the agent already knows what it asked for (the selector, the
|
|
68
|
+
* text length, the key, the drag endpoints); echoing that back N times in
|
|
69
|
+
* a row is not worth the tokens. `settle` is kept because it carries NEW
|
|
70
|
+
* information (did the page go quiet, did focus/URL drift), and so are a
|
|
71
|
+
* click result's `hit_element` — where the click actually landed when
|
|
72
|
+
* that differs from the resolved target (see `ClickStepResult`) — and a
|
|
73
|
+
* drag result's `drag_mode` — whether HTML5 native drag or pointer-only
|
|
74
|
+
* events fired (see `DragStepResult`). */
|
|
62
75
|
function compactActionResult(result) {
|
|
63
|
-
return
|
|
76
|
+
return {
|
|
77
|
+
ok: true,
|
|
78
|
+
...(result.settle ? { settle: result.settle } : {}),
|
|
79
|
+
...(result.hit_element ? { hit_element: result.hit_element } : {}),
|
|
80
|
+
...(result.drag_mode ? { drag_mode: result.drag_mode } : {}),
|
|
81
|
+
};
|
|
64
82
|
}
|
|
65
83
|
async function withRecoverySnapshot(deps, index, kind, error, stepsCompleted) {
|
|
66
84
|
let recovery_snapshot = null;
|
|
@@ -127,7 +145,7 @@ export async function runFlow(steps, deps) {
|
|
|
127
145
|
const raw = steps[i];
|
|
128
146
|
const kind = stepKind(raw);
|
|
129
147
|
if (!kind) {
|
|
130
|
-
return withRecoverySnapshot(deps, i, 'unknown', 'flow: each step must be exactly one of find | click | type | press | wait_for', results.length);
|
|
148
|
+
return withRecoverySnapshot(deps, i, 'unknown', 'flow: each step must be exactly one of find | click | type | press | wait_for | drag', results.length);
|
|
131
149
|
}
|
|
132
150
|
const stepRecord = raw;
|
|
133
151
|
if (kind === 'find') {
|
|
@@ -166,6 +184,25 @@ export async function runFlow(steps, deps) {
|
|
|
166
184
|
results.push({ ok: true });
|
|
167
185
|
continue;
|
|
168
186
|
}
|
|
187
|
+
if (kind === 'drag') {
|
|
188
|
+
const params = stepRecord.drag;
|
|
189
|
+
// No resolveTarget here ON PURPOSE: a drag names BOTH endpoints
|
|
190
|
+
// explicitly (selector or coords), so there is no single `selector`
|
|
191
|
+
// slot the implicit target could unambiguously fill — a pending
|
|
192
|
+
// implicit target survives this step untouched for a later
|
|
193
|
+
// click/type/press, exactly like it survives a wait_for.
|
|
194
|
+
const outcome = await deps.performDrag(params);
|
|
195
|
+
if (!outcome.ok) {
|
|
196
|
+
return withRecoverySnapshot(deps, i, kind, outcome.error + raceHint(), results.length);
|
|
197
|
+
}
|
|
198
|
+
// A successful drag resolved both endpoints via an in-page probe,
|
|
199
|
+
// which proves the document is reachable again after any earlier
|
|
200
|
+
// navigation — clear the race hint like find/wait_for do. (Drag has
|
|
201
|
+
// no settle phase, so there is no settle reason to carry instead.)
|
|
202
|
+
lastSettleReason = undefined;
|
|
203
|
+
results.push(compactActionResult(outcome.result));
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
169
206
|
if (kind === 'click') {
|
|
170
207
|
const params = stepRecord.click;
|
|
171
208
|
const { resolved, consumed } = resolveTarget(params.selector, implicitTarget);
|
package/dist/cdp/flow.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"flow.js","sourceRoot":"","sources":["../../src/cdp/flow.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"flow.js","sourceRoot":"","sources":["../../src/cdp/flow.ts"],"names":[],"mappings":"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"}
|
|
@@ -12,6 +12,11 @@
|
|
|
12
12
|
* optional `frame` field with the innermost hosting iframe's selector, and
|
|
13
13
|
* entries whose generated selector could not be made unique across the deep
|
|
14
14
|
* search scope carry `ambiguous: true` (see `genSelectorInfo`).
|
|
15
|
+
*
|
|
16
|
+
* Interactive entries are gated by `isClickTarget`, not bare `isVisible`:
|
|
17
|
+
* a computed `pointer-events: none` element can never receive a real click
|
|
18
|
+
* (see the helper's comment in `dom-helpers.ts`). Headings and the text
|
|
19
|
+
* dump keep plain `isVisible`, so such elements still contribute text.
|
|
15
20
|
*/
|
|
16
21
|
export interface SnapshotOpts {
|
|
17
22
|
within_selector?: string;
|
|
@@ -27,7 +32,9 @@ export declare function buildSnapshotJs(opts?: SnapshotOpts): string;
|
|
|
27
32
|
*
|
|
28
33
|
* Role-aware: when `role` is provided, only elements whose explicit ARIA
|
|
29
34
|
* role or implicit role match are considered. When omitted, the search
|
|
30
|
-
* scans a broad set of interactive + clickable elements.
|
|
35
|
+
* scans a broad set of interactive + clickable elements. Candidates are
|
|
36
|
+
* gated by `isClickTarget` — a pointer-events:none element is never a
|
|
37
|
+
* match; it can still surface as a near-miss hint (plain `isVisible`).
|
|
31
38
|
*
|
|
32
39
|
* The scan pierces open Shadow DOM roots and same-origin iframes via
|
|
33
40
|
* `deepQueryAll`. `coords` are mapped to TOP-LEVEL viewport coordinates
|
|
@@ -62,11 +69,19 @@ export declare function buildFindJs(opts: FindOpts): string;
|
|
|
62
69
|
* view, computes the TOP-LEVEL viewport click point, and — unless `force`
|
|
63
70
|
* is set — hit-tests that point before returning it, so a covering overlay
|
|
64
71
|
* is caught before dispatching CDP mouse events. Returns:
|
|
65
|
-
* { ok: true, x, y, tag }
|
|
72
|
+
* { ok: true, x, y, tag, hit_element? }
|
|
66
73
|
* { ok: false, reason: 'invalid-selector', error }
|
|
67
74
|
* { ok: false, reason: 'not-found' }
|
|
68
75
|
* { ok: false, reason: 'occluded', blocker }
|
|
69
76
|
*
|
|
77
|
+
* `hit_element` is present ONLY when the dispatched click will land on a
|
|
78
|
+
* DIFFERENT element than the resolved target (neither the target nor a
|
|
79
|
+
* descendant): a pointer-events:none target whose real hit-target sibling
|
|
80
|
+
* takes the click (allowed, not 'occluded' — see checkOcclusion), an
|
|
81
|
+
* ancestor wrapper, or a `force` click landing on whatever covers the
|
|
82
|
+
* point — the hit-test runs under `force` too, which skips the BLOCK,
|
|
83
|
+
* not the truth-telling.
|
|
84
|
+
*
|
|
70
85
|
* The `invalid-selector` check runs BEFORE `deepQueryFirst` — a malformed
|
|
71
86
|
* CSS selector throws the identical `SyntaxError` from every root's own
|
|
72
87
|
* `querySelector`, which `deepQueryFirst`'s per-root try/catch otherwise
|
|
@@ -81,7 +81,9 @@ export function buildSnapshotJs(opts = {}) {
|
|
|
81
81
|
const sel = 'a[href], button, input, select, textarea, [role=button], [role=link], [role=checkbox], [role=tab], [role=menuitem], [contenteditable=true]';
|
|
82
82
|
const interactive = [];
|
|
83
83
|
deepQueryAll(sel, root).forEach((el) => {
|
|
84
|
-
|
|
84
|
+
// isClickTarget, not isVisible: a pointer-events:none element can
|
|
85
|
+
// never receive a real click. Headings/text keep plain isVisible.
|
|
86
|
+
if (!isClickTarget(el)) return;
|
|
85
87
|
if (inExcludedLandmark(el)) return;
|
|
86
88
|
const tag = el.tagName.toLowerCase();
|
|
87
89
|
const role = el.getAttribute('role') || tag;
|
|
@@ -159,7 +161,9 @@ export function buildFindJs(opts) {
|
|
|
159
161
|
const all = deepQueryAll(selectorSet);
|
|
160
162
|
const matches = [];
|
|
161
163
|
for (const el of all) {
|
|
162
|
-
|
|
164
|
+
// isClickTarget, not isVisible: find returns CLICK TARGETS. Near-miss
|
|
165
|
+
// ranking below keeps plain isVisible — hints, never click targets.
|
|
166
|
+
if (!isClickTarget(el)) continue;
|
|
163
167
|
const text = accessibleText(el).trim();
|
|
164
168
|
if (text.length === 0) continue;
|
|
165
169
|
const lower = text.toLowerCase();
|
|
@@ -292,13 +296,14 @@ export function buildClickResolveJs(opts) {
|
|
|
292
296
|
if (el.scrollIntoView) el.scrollIntoView({ block: 'center', inline: 'center' });
|
|
293
297
|
const center = viewportCenterOf(el);
|
|
294
298
|
const tag = el.tagName.toLowerCase();
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
return { ok: false, reason: 'occluded', blocker: occlusion.blocker };
|
|
299
|
-
}
|
|
299
|
+
const occlusion = checkOcclusion(el, center.localX, center.localY);
|
|
300
|
+
if (!occlusion.allowed && !opts.force) {
|
|
301
|
+
return { ok: false, reason: 'occluded', blocker: occlusion.blocker };
|
|
300
302
|
}
|
|
301
|
-
|
|
303
|
+
const result = { ok: true, x: center.x, y: center.y, tag };
|
|
304
|
+
const hit = occlusion.allowed ? occlusion.hit : occlusion.blocker;
|
|
305
|
+
if (hit) result.hit_element = hit;
|
|
306
|
+
return result;
|
|
302
307
|
})()
|
|
303
308
|
`;
|
|
304
309
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"builders.js","sourceRoot":"","sources":["../../../src/cdp/inpage/builders.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"builders.js","sourceRoot":"","sources":["../../../src/cdp/inpage/builders.ts"],"names":[],"mappings":"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"}
|
|
@@ -54,4 +54,4 @@
|
|
|
54
54
|
* `dom-helpers.ts`), snapshot/find mark the affected entry with
|
|
55
55
|
* `ambiguous: true` so agents know not to cache that selector.
|
|
56
56
|
*/
|
|
57
|
-
export declare const DEEP_QUERY_JS = "\n function collectSearchRoots(start) {\n // start: a Document, Element or ShadowRoot to scope the search to.\n // Returns [start, ...every open shadow root and same-origin iframe\n // document reachable inside start's subtree, recursively]. The caller\n // runs .querySelectorAll()/.querySelector() on each entry directly \u2014\n // each entry's own querySelectorAll does NOT pierce into nested shadow\n // roots or iframes on its own, which is why we enumerate them here.\n var roots = [start];\n function walk(scope, depth) {\n // Depth guard, same pattern as the other recursive helpers here:\n // pathological nesting (or a cycle introduced by a broken mock)\n // must degrade to a truncated result, never a stack overflow.\n if (depth > 20) return;\n var all;\n try {\n all = scope.querySelectorAll('*');\n } catch (_) {\n return;\n }\n for (var i = 0; i < all.length; i++) {\n var el = all[i];\n if (el.shadowRoot) {\n roots.push(el.shadowRoot);\n walk(el.shadowRoot, depth + 1);\n }\n if (el.tagName === 'IFRAME') {\n var doc = null;\n try {\n doc = el.contentDocument;\n } catch (_) {\n doc = null;\n }\n if (doc) {\n roots.push(doc);\n walk(doc, depth + 1);\n }\n }\n }\n }\n walk(start, 0);\n return roots;\n }\n\n function deepWalkAll(callback, start) {\n // Tree-walk primitive: invoke callback(el) for every element reachable\n // from start (default: document) across the top document, every open\n // shadow root, and every same-origin iframe document.\n var root = start || document;\n var roots = collectSearchRoots(root);\n for (var r = 0; r < roots.length; r++) {\n var all;\n try {\n all = roots[r].querySelectorAll('*');\n } catch (_) {\n continue;\n }\n for (var i = 0; i < all.length; i++) callback(all[i]);\n }\n }\n\n function deepQueryAll(selector, start) {\n var root = start || document;\n var roots = collectSearchRoots(root);\n var out = [];\n for (var r = 0; r < roots.length; r++) {\n var found;\n try {\n found = roots[r].querySelectorAll(selector);\n } catch (_) {\n continue;\n }\n for (var i = 0; i < found.length; i++) out.push(found[i]);\n }\n return out;\n }\n\n function deepQueryFirst(selector, start) {\n var root = start || document;\n var roots = collectSearchRoots(root);\n for (var r = 0; r < roots.length; r++) {\n var el = null;\n try {\n el = roots[r].querySelector(selector);\n } catch (_) {\n el = null;\n }\n if (el) return el;\n }\n return null;\n }\n\n function frameElementChain(el) {\n // Walk up from el's own window to the top window, collecting the\n // <iframe> element that hosts each intermediate document. Outermost\n // frame first. Same-origin only \u2014 frameElement throws/returns null\n // across an origin boundary, at which point the chain simply stops\n // (the coordinate math below degrades gracefully rather than throwing).\n var chain = [];\n var doc = el.ownerDocument;\n var win = doc && doc.defaultView;\n if (!win) return chain;\n var topWin;\n try {\n topWin = win.top;\n } catch (_) {\n topWin = win;\n }\n var guard = 0;\n while (win && win !== topWin && guard < 20) {\n guard++;\n var fe = null;\n try {\n fe = win.frameElement;\n } catch (_) {\n fe = null;\n }\n if (!fe) break;\n chain.unshift(fe);\n win = fe.ownerDocument && fe.ownerDocument.defaultView;\n }\n return chain;\n }\n\n function frameViewportOriginOffset(fe) {\n // The embedded document's viewport origin sits at the frame element's\n // CONTENT box, but getBoundingClientRect() returns the BORDER box \u2014\n // border and padding must be added on top of rect.left/rect.top or\n // every coordinate inside a styled iframe lands short. clientLeft/\n // clientTop are the border widths; padding comes from computed style\n // (read through the frame's own view \u2014 same-origin, so reachable).\n var borderX = fe.clientLeft || 0;\n var borderY = fe.clientTop || 0;\n var padX = 0;\n var padY = 0;\n try {\n var view = (fe.ownerDocument && fe.ownerDocument.defaultView) || window;\n var style = view.getComputedStyle(fe);\n padX = parseFloat(style.paddingLeft) || 0;\n padY = parseFloat(style.paddingTop) || 0;\n } catch (_) {}\n return { x: borderX + padX, y: borderY + padY };\n }\n\n function clickRectOf(el) {\n // For a line-wrapped INLINE element (a link spanning two lines), the\n // center of getBoundingClientRect() can land in the unpainted gap\n // between the line boxes \u2014 elementFromPoint would report whatever sits\n // behind, and a dispatched click would miss. The first client rect is\n // always a painted fragment, so its center is a safe click point.\n // Block-level elements have exactly one client rect, identical to the\n // bounding rect \u2014 no behavior change for them.\n var rects = null;\n try {\n rects = el.getClientRects ? el.getClientRects() : null;\n } catch (_) {\n rects = null;\n }\n if (rects && rects.length > 0) return rects[0];\n return el.getBoundingClientRect();\n }\n\n function viewportCenterOf(el) {\n // Returns the element's click point in two coordinate spaces:\n // - localX/localY: relative to el's OWN document viewport (what\n // that document's elementFromPoint expects).\n // - x/y: relative to the TOP-LEVEL viewport (what CDP\n // Input.dispatchMouseEvent expects) \u2014 localX/localY plus the\n // accumulated content-box origin of every ancestor <iframe>\n // element (border-box rect + border + padding). Shadow DOM\n // boundaries need no offset: they do not introduce a new\n // coordinate space.\n // Both spaces derive from the SAME point (first-client-rect center),\n // so the occlusion hit-test and the CDP dispatch always agree.\n var rect = clickRectOf(el);\n var localX = rect.left + rect.width / 2;\n var localY = rect.top + rect.height / 2;\n var chain = frameElementChain(el);\n var offsetX = 0;\n var offsetY = 0;\n for (var i = 0; i < chain.length; i++) {\n var frameRect = chain[i].getBoundingClientRect();\n var origin = frameViewportOriginOffset(chain[i]);\n offsetX += frameRect.left + origin.x;\n offsetY += frameRect.top + origin.y;\n }\n return { x: offsetX + localX, y: offsetY + localY, localX: localX, localY: localY };\n }\n\n function composedContains(ancestor, node) {\n // Node.contains() does not cross shadow boundaries. This walks the\n // COMPOSED tree: when climbing hits a ShadowRoot (no parentNode), jump\n // to its host and keep climbing.\n var cur = node;\n var guard = 0;\n while (cur && guard < 1000) {\n guard++;\n if (cur === ancestor) return true;\n if (cur.parentNode) {\n cur = cur.parentNode;\n continue;\n }\n if (cur.host) {\n cur = cur.host;\n continue;\n }\n break;\n }\n return false;\n }\n\n function describeElement(el) {\n if (!el || !el.tagName) return 'an unknown element';\n var desc = el.tagName.toLowerCase();\n if (el.id) desc += '#' + el.id;\n if (el.classList && el.classList.length) {\n var classes = [];\n for (var i = 0; i < el.classList.length && i < 2; i++) classes.push(el.classList[i]);\n if (classes.length) desc += '.' + classes.join('.');\n }\n return desc;\n }\n\n function hitTestDeep(root, x, y) {\n var node = null;\n try {\n node = root && root.elementFromPoint ? root.elementFromPoint(x, y) : null;\n } catch (_) {\n node = null;\n }\n // The hit element may itself host a nested open shadow root whose\n // content visually covers the point \u2014 descend until elementFromPoint\n // stops finding a deeper shadow tree.\n var guard = 0;\n while (node && node.shadowRoot && guard < 20) {\n guard++;\n var inner = null;\n try {\n inner = node.shadowRoot.elementFromPoint ? node.shadowRoot.elementFromPoint(x, y) : null;\n } catch (_) {\n inner = null;\n }\n if (!inner || inner === node) break;\n node = inner;\n }\n return node;\n }\n\n function checkOcclusion(target, localX, localY) {\n // Hit-test starting from the element's OWN root (its closest shadow\n // root, or the document that owns it when not in shadow DOM) using\n // LOCAL coordinates \u2014 the same coordinate space that root's own\n // elementFromPoint expects (an iframe document's viewport starts at\n // its own top-left, not the top-level page's).\n var root = target.getRootNode ? target.getRootNode() : document;\n var hit = hitTestDeep(root, localX, localY);\n if (!hit) {\n // Nothing hit-tested at all (e.g. elementFromPoint unsupported in\n // this environment, or the point is outside the rendered area).\n // Fail open rather than block a click we cannot actually verify.\n return { allowed: true };\n }\n if (hit === target || composedContains(target, hit) || composedContains(hit, target)) {\n return { allowed: true };\n }\n return { allowed: false, blocker: describeElement(hit) };\n }\n";
|
|
57
|
+
export declare const DEEP_QUERY_JS = "\n function collectSearchRoots(start) {\n // start: a Document, Element or ShadowRoot to scope the search to.\n // Returns [start, ...every open shadow root and same-origin iframe\n // document reachable inside start's subtree, recursively]. The caller\n // runs .querySelectorAll()/.querySelector() on each entry directly \u2014\n // each entry's own querySelectorAll does NOT pierce into nested shadow\n // roots or iframes on its own, which is why we enumerate them here.\n var roots = [start];\n function walk(scope, depth) {\n // Depth guard, same pattern as the other recursive helpers here:\n // pathological nesting (or a cycle introduced by a broken mock)\n // must degrade to a truncated result, never a stack overflow.\n if (depth > 20) return;\n var all;\n try {\n all = scope.querySelectorAll('*');\n } catch (_) {\n return;\n }\n for (var i = 0; i < all.length; i++) {\n var el = all[i];\n if (el.shadowRoot) {\n roots.push(el.shadowRoot);\n walk(el.shadowRoot, depth + 1);\n }\n if (el.tagName === 'IFRAME') {\n var doc = null;\n try {\n doc = el.contentDocument;\n } catch (_) {\n doc = null;\n }\n if (doc) {\n roots.push(doc);\n walk(doc, depth + 1);\n }\n }\n }\n }\n walk(start, 0);\n return roots;\n }\n\n function deepWalkAll(callback, start) {\n // Tree-walk primitive: invoke callback(el) for every element reachable\n // from start (default: document) across the top document, every open\n // shadow root, and every same-origin iframe document.\n var root = start || document;\n var roots = collectSearchRoots(root);\n for (var r = 0; r < roots.length; r++) {\n var all;\n try {\n all = roots[r].querySelectorAll('*');\n } catch (_) {\n continue;\n }\n for (var i = 0; i < all.length; i++) callback(all[i]);\n }\n }\n\n function deepQueryAll(selector, start) {\n var root = start || document;\n var roots = collectSearchRoots(root);\n var out = [];\n for (var r = 0; r < roots.length; r++) {\n var found;\n try {\n found = roots[r].querySelectorAll(selector);\n } catch (_) {\n continue;\n }\n for (var i = 0; i < found.length; i++) out.push(found[i]);\n }\n return out;\n }\n\n function deepQueryFirst(selector, start) {\n var root = start || document;\n var roots = collectSearchRoots(root);\n for (var r = 0; r < roots.length; r++) {\n var el = null;\n try {\n el = roots[r].querySelector(selector);\n } catch (_) {\n el = null;\n }\n if (el) return el;\n }\n return null;\n }\n\n function frameElementChain(el) {\n // Walk up from el's own window to the top window, collecting the\n // <iframe> element that hosts each intermediate document. Outermost\n // frame first. Same-origin only \u2014 frameElement throws/returns null\n // across an origin boundary, at which point the chain simply stops\n // (the coordinate math below degrades gracefully rather than throwing).\n var chain = [];\n var doc = el.ownerDocument;\n var win = doc && doc.defaultView;\n if (!win) return chain;\n var topWin;\n try {\n topWin = win.top;\n } catch (_) {\n topWin = win;\n }\n var guard = 0;\n while (win && win !== topWin && guard < 20) {\n guard++;\n var fe = null;\n try {\n fe = win.frameElement;\n } catch (_) {\n fe = null;\n }\n if (!fe) break;\n chain.unshift(fe);\n win = fe.ownerDocument && fe.ownerDocument.defaultView;\n }\n return chain;\n }\n\n function frameViewportOriginOffset(fe) {\n // The embedded document's viewport origin sits at the frame element's\n // CONTENT box, but getBoundingClientRect() returns the BORDER box \u2014\n // border and padding must be added on top of rect.left/rect.top or\n // every coordinate inside a styled iframe lands short. clientLeft/\n // clientTop are the border widths; padding comes from computed style\n // (read through the frame's own view \u2014 same-origin, so reachable).\n var borderX = fe.clientLeft || 0;\n var borderY = fe.clientTop || 0;\n var padX = 0;\n var padY = 0;\n try {\n var view = (fe.ownerDocument && fe.ownerDocument.defaultView) || window;\n var style = view.getComputedStyle(fe);\n padX = parseFloat(style.paddingLeft) || 0;\n padY = parseFloat(style.paddingTop) || 0;\n } catch (_) {}\n return { x: borderX + padX, y: borderY + padY };\n }\n\n function clickRectOf(el) {\n // For a line-wrapped INLINE element (a link spanning two lines), the\n // center of getBoundingClientRect() can land in the unpainted gap\n // between the line boxes \u2014 elementFromPoint would report whatever sits\n // behind, and a dispatched click would miss. The first client rect is\n // always a painted fragment, so its center is a safe click point.\n // Block-level elements have exactly one client rect, identical to the\n // bounding rect \u2014 no behavior change for them.\n var rects = null;\n try {\n rects = el.getClientRects ? el.getClientRects() : null;\n } catch (_) {\n rects = null;\n }\n if (rects && rects.length > 0) return rects[0];\n return el.getBoundingClientRect();\n }\n\n function viewportCenterOf(el) {\n // Returns the element's click point in two coordinate spaces:\n // - localX/localY: relative to el's OWN document viewport (what\n // that document's elementFromPoint expects).\n // - x/y: relative to the TOP-LEVEL viewport (what CDP\n // Input.dispatchMouseEvent expects) \u2014 localX/localY plus the\n // accumulated content-box origin of every ancestor <iframe>\n // element (border-box rect + border + padding). Shadow DOM\n // boundaries need no offset: they do not introduce a new\n // coordinate space.\n // Both spaces derive from the SAME point (first-client-rect center),\n // so the occlusion hit-test and the CDP dispatch always agree.\n var rect = clickRectOf(el);\n var localX = rect.left + rect.width / 2;\n var localY = rect.top + rect.height / 2;\n var chain = frameElementChain(el);\n var offsetX = 0;\n var offsetY = 0;\n for (var i = 0; i < chain.length; i++) {\n var frameRect = chain[i].getBoundingClientRect();\n var origin = frameViewportOriginOffset(chain[i]);\n offsetX += frameRect.left + origin.x;\n offsetY += frameRect.top + origin.y;\n }\n return { x: offsetX + localX, y: offsetY + localY, localX: localX, localY: localY };\n }\n\n function composedContains(ancestor, node) {\n // Node.contains() does not cross shadow boundaries. This walks the\n // COMPOSED tree: when climbing hits a ShadowRoot (no parentNode), jump\n // to its host and keep climbing.\n var cur = node;\n var guard = 0;\n while (cur && guard < 1000) {\n guard++;\n if (cur === ancestor) return true;\n if (cur.parentNode) {\n cur = cur.parentNode;\n continue;\n }\n if (cur.host) {\n cur = cur.host;\n continue;\n }\n break;\n }\n return false;\n }\n\n function describeElement(el) {\n if (!el || !el.tagName) return 'an unknown element';\n var desc = el.tagName.toLowerCase();\n if (el.id) desc += '#' + el.id;\n if (el.classList && el.classList.length) {\n var classes = [];\n for (var i = 0; i < el.classList.length && i < 2; i++) classes.push(el.classList[i]);\n if (classes.length) desc += '.' + classes.join('.');\n }\n return desc;\n }\n\n function hitTestDeep(root, x, y) {\n var node = null;\n try {\n node = root && root.elementFromPoint ? root.elementFromPoint(x, y) : null;\n } catch (_) {\n node = null;\n }\n // The hit element may itself host a nested open shadow root whose\n // content visually covers the point \u2014 descend until elementFromPoint\n // stops finding a deeper shadow tree.\n var guard = 0;\n while (node && node.shadowRoot && guard < 20) {\n guard++;\n var inner = null;\n try {\n inner = node.shadowRoot.elementFromPoint ? node.shadowRoot.elementFromPoint(x, y) : null;\n } catch (_) {\n inner = null;\n }\n if (!inner || inner === node) break;\n node = inner;\n }\n return node;\n }\n\n function computedPointerEvents(el) {\n // Read through the element's own window (cross-document\n // getComputedStyle is unreliable); any failure fails open (not 'none').\n try {\n var view = (el.ownerDocument && el.ownerDocument.defaultView) || window;\n return view.getComputedStyle(el).pointerEvents;\n } catch (_) {\n return '';\n }\n }\n\n function checkOcclusion(target, localX, localY) {\n // Hit-test starting from the element's OWN root (its closest shadow\n // root, or the document that owns it when not in shadow DOM) using\n // LOCAL coordinates \u2014 the same coordinate space that root's own\n // elementFromPoint expects (an iframe document's viewport starts at\n // its own top-left, not the top-level page's).\n var root = target.getRootNode ? target.getRootNode() : document;\n var hit = hitTestDeep(root, localX, localY);\n if (!hit) {\n // Nothing hit-tested at all (e.g. elementFromPoint unsupported in\n // this environment, or the point is outside the rendered area).\n // Fail open rather than block a click we cannot actually verify.\n return { allowed: true };\n }\n if (hit === target || composedContains(target, hit)) {\n return { allowed: true };\n }\n if (composedContains(hit, target)) {\n // ANCESTOR hit: the click targets the ancestor and never reaches\n // the element itself (events bubble upward, not down). Allowed as\n // before, but surfaced so click results can report hit_element.\n return { allowed: true, hit: describeElement(hit) };\n }\n if (computedPointerEvents(target) === 'none') {\n // A pointer-events:none target can never be the hit-test result \u2014 a\n // real user click at this exact point reaches hit instead, by the\n // page's own design (invisible a11y layer over the visual control's\n // hit-target sibling, e.g. Articulate Storyline). The hit is not a\n // blocker to dismiss; it IS what a real click activates, so allow\n // the dispatch and report the true recipient.\n return { allowed: true, hit: describeElement(hit) };\n }\n return { allowed: false, blocker: describeElement(hit) };\n }\n";
|
|
@@ -302,6 +302,17 @@ export const DEEP_QUERY_JS = `
|
|
|
302
302
|
return node;
|
|
303
303
|
}
|
|
304
304
|
|
|
305
|
+
function computedPointerEvents(el) {
|
|
306
|
+
// Read through the element's own window (cross-document
|
|
307
|
+
// getComputedStyle is unreliable); any failure fails open (not 'none').
|
|
308
|
+
try {
|
|
309
|
+
var view = (el.ownerDocument && el.ownerDocument.defaultView) || window;
|
|
310
|
+
return view.getComputedStyle(el).pointerEvents;
|
|
311
|
+
} catch (_) {
|
|
312
|
+
return '';
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
305
316
|
function checkOcclusion(target, localX, localY) {
|
|
306
317
|
// Hit-test starting from the element's OWN root (its closest shadow
|
|
307
318
|
// root, or the document that owns it when not in shadow DOM) using
|
|
@@ -316,9 +327,24 @@ export const DEEP_QUERY_JS = `
|
|
|
316
327
|
// Fail open rather than block a click we cannot actually verify.
|
|
317
328
|
return { allowed: true };
|
|
318
329
|
}
|
|
319
|
-
if (hit === target || composedContains(target, hit)
|
|
330
|
+
if (hit === target || composedContains(target, hit)) {
|
|
320
331
|
return { allowed: true };
|
|
321
332
|
}
|
|
333
|
+
if (composedContains(hit, target)) {
|
|
334
|
+
// ANCESTOR hit: the click targets the ancestor and never reaches
|
|
335
|
+
// the element itself (events bubble upward, not down). Allowed as
|
|
336
|
+
// before, but surfaced so click results can report hit_element.
|
|
337
|
+
return { allowed: true, hit: describeElement(hit) };
|
|
338
|
+
}
|
|
339
|
+
if (computedPointerEvents(target) === 'none') {
|
|
340
|
+
// A pointer-events:none target can never be the hit-test result — a
|
|
341
|
+
// real user click at this exact point reaches hit instead, by the
|
|
342
|
+
// page's own design (invisible a11y layer over the visual control's
|
|
343
|
+
// hit-target sibling, e.g. Articulate Storyline). The hit is not a
|
|
344
|
+
// blocker to dismiss; it IS what a real click activates, so allow
|
|
345
|
+
// the dispatch and report the true recipient.
|
|
346
|
+
return { allowed: true, hit: describeElement(hit) };
|
|
347
|
+
}
|
|
322
348
|
return { allowed: false, blocker: describeElement(hit) };
|
|
323
349
|
}
|
|
324
350
|
`;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deep-query.js","sourceRoot":"","sources":["../../../src/cdp/inpage/deep-query.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG
|
|
1
|
+
{"version":3,"file":"deep-query.js","sourceRoot":"","sources":["../../../src/cdp/inpage/deep-query.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqS5B,CAAC"}
|
|
@@ -24,4 +24,4 @@
|
|
|
24
24
|
* snapshot/find builders surface it on the affected entry so agents know
|
|
25
25
|
* that selector resolves first-match-wins and must not be cached.
|
|
26
26
|
*/
|
|
27
|
-
export declare const DOM_HELPERS_JS = "\n function isVisible(el) {\n if (!(el instanceof HTMLElement)) return true;\n const style = getComputedStyle(el);\n if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return false;\n if (el.offsetParent === null && style.position !== 'fixed') return false;\n return true;\n }\n function shortText(el) {\n const t = (el.innerText || el.textContent || '').trim();\n return t.length > 120 ? t.slice(0, 120) + '...' : t;\n }\n function safeCss(s) {\n return s.replace(/\"/g, '\\\\\"');\n }\n function genSelectorInfo(el) {\n // Uniqueness is verified with deepQueryAll \u2014 an id/testid/aria-label/\n // name that is unique in the top document can still collide with an\n // identically-attributed element inside a shadow root or same-origin\n // iframe, since each root has its own independent attribute namespace.\n if (el.id && !/^[\\d]/.test(el.id) && !/\\s/.test(el.id)) {\n try {\n const candidate = '#' + CSS.escape(el.id);\n if (deepQueryAll(candidate).length === 1) return { selector: candidate, ambiguous: false };\n } catch (_) {}\n }\n const tid = el.getAttribute('data-testid');\n if (tid) {\n const candidate = '[data-testid=\"' + safeCss(tid) + '\"]';\n if (deepQueryAll(candidate).length === 1) return { selector: candidate, ambiguous: false };\n }\n const al = el.getAttribute('aria-label');\n if (al && al.length < 60) {\n const candidate = el.tagName.toLowerCase() + '[aria-label=\"' + safeCss(al) + '\"]';\n if (deepQueryAll(candidate).length === 1) return { selector: candidate, ambiguous: false };\n }\n const name = el.getAttribute('name');\n if (name && (el.tagName === 'INPUT' || el.tagName === 'SELECT' || el.tagName === 'TEXTAREA')) {\n const candidate = el.tagName.toLowerCase() + '[name=\"' + safeCss(name) + '\"]';\n if (deepQueryAll(candidate).length === 1) return { selector: candidate, ambiguous: false };\n }\n // Structural fallback, verified like every shortcut above. The short\n // form (6 parts, indexed only where siblings force it) is preferred\n // for stability; when it matches more than one element across roots,\n // retry with a fully-qualified path (every level :nth-of-type-indexed,\n // climbed all the way to the root boundary).\n const structural = buildStructuralSelector(el, 6, false);\n try {\n if (deepQueryAll(structural).length === 1) return { selector: structural, ambiguous: false };\n } catch (_) {}\n const qualified = buildStructuralSelector(el, 32, true);\n try {\n if (deepQueryAll(qualified).length === 1) return { selector: qualified, ambiguous: false };\n } catch (_) {}\n // Still ambiguous: structurally-identical twins in different roots.\n // No CSS syntax can scope a selector to one shadow root / iframe, so\n // return the most qualified path and flag it \u2014 deepQueryFirst will\n // resolve it first-match-wins (deterministic traversal order), and\n // the flag tells agents not to cache it.\n return { selector: qualified, ambiguous: true };\n }\n function genSelector(el) {\n return genSelectorInfo(el).selector;\n }\n function buildStructuralSelector(el, maxParts, alwaysIndex) {\n // Local-root-scoped structural path: CLIMBING stops at a shadow root or\n // document boundary (parentElement is null for a shadow root's direct\n // children) \u2014 matches how deepQueryAll scopes each root's own\n // querySelectorAll call, so the path resolves back to the same element\n // when re-queried through deepQueryFirst. Sibling lookup for the\n // :nth-of-type qualifier uses parentNode rather than parentElement so a\n // shadow root's own top-level children (parentNode = the ShadowRoot,\n // which still exposes .children) get disambiguated too, even though we\n // do not climb past that boundary. With alwaysIndex, every level gets\n // an explicit :nth-of-type \u2014 maximum qualification for the retry pass.\n const parts = [];\n let cur = el;\n while (cur && cur.nodeType === 1 && cur !== document.body && parts.length < maxParts) {\n let part = cur.tagName.toLowerCase();\n const parentNode = cur.parentNode;\n if (parentNode && parentNode.children) {\n const sib = Array.from(parentNode.children).filter(s => s.tagName === cur.tagName);\n if (sib.length > 1 || alwaysIndex) {\n part += ':nth-of-type(' + (sib.indexOf(cur) + 1) + ')';\n }\n }\n parts.unshift(part);\n cur = cur.parentElement;\n }\n return parts.join(' > ');\n }\n function accessibleText(el) {\n const aria = el.getAttribute && el.getAttribute('aria-label');\n if (aria) return aria;\n const txt = (el.innerText || el.textContent || '').trim();\n if (txt) return txt;\n if ('value' in el && el.value) return String(el.value);\n const ph = el.getAttribute && el.getAttribute('placeholder');\n if (ph) return ph;\n const title = el.getAttribute && el.getAttribute('title');\n if (title) return title;\n return '';\n }\n function frameSelectorFor(el) {\n // Lightweight context field for snapshot/find entries: the selector of\n // the innermost same-origin <iframe> hosting el, or null when el lives\n // in the top document. Computed via genSelector so it benefits from the\n // same deep-uniqueness verification.\n if (el.ownerDocument === document) return null;\n const win = el.ownerDocument && el.ownerDocument.defaultView;\n let fe = null;\n try {\n fe = win && win.frameElement;\n } catch (_) {\n fe = null;\n }\n if (!fe) return null;\n return genSelector(fe);\n }\n";
|
|
27
|
+
export declare const DOM_HELPERS_JS = "\n function isVisible(el) {\n if (!(el instanceof HTMLElement)) return true;\n const style = getComputedStyle(el);\n if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return false;\n if (el.offsetParent === null && style.position !== 'fixed') return false;\n return true;\n }\n function isClickTarget(el) {\n // A computed pointer-events:none element can never be the browser's\n // hit-test result \u2014 frameworks with an invisible accessibility layer\n // (e.g. Articulate Storyline's #acc-* elements) stack such elements\n // over the real hit-target sibling, so advertising one produces\n // clicks that report success but land elsewhere. Per-element computed\n // check, NOT a subtree prune: a descendant can re-enable itself with\n // pointer-events: auto and IS a valid target. Only the interactive\n // collections use this gate \u2014 headings/text keep plain isVisible.\n if (!isVisible(el)) return false;\n try {\n const view = (el.ownerDocument && el.ownerDocument.defaultView) || window;\n if (view.getComputedStyle(el).pointerEvents === 'none') return false;\n } catch (_) {}\n return true;\n }\n function shortText(el) {\n const t = (el.innerText || el.textContent || '').trim();\n return t.length > 120 ? t.slice(0, 120) + '...' : t;\n }\n function safeCss(s) {\n return s.replace(/\"/g, '\\\\\"');\n }\n function genSelectorInfo(el) {\n // Uniqueness is verified with deepQueryAll \u2014 an id/testid/aria-label/\n // name that is unique in the top document can still collide with an\n // identically-attributed element inside a shadow root or same-origin\n // iframe, since each root has its own independent attribute namespace.\n if (el.id && !/^[\\d]/.test(el.id) && !/\\s/.test(el.id)) {\n try {\n const candidate = '#' + CSS.escape(el.id);\n if (deepQueryAll(candidate).length === 1) return { selector: candidate, ambiguous: false };\n } catch (_) {}\n }\n const tid = el.getAttribute('data-testid');\n if (tid) {\n const candidate = '[data-testid=\"' + safeCss(tid) + '\"]';\n if (deepQueryAll(candidate).length === 1) return { selector: candidate, ambiguous: false };\n }\n const al = el.getAttribute('aria-label');\n if (al && al.length < 60) {\n const candidate = el.tagName.toLowerCase() + '[aria-label=\"' + safeCss(al) + '\"]';\n if (deepQueryAll(candidate).length === 1) return { selector: candidate, ambiguous: false };\n }\n const name = el.getAttribute('name');\n if (name && (el.tagName === 'INPUT' || el.tagName === 'SELECT' || el.tagName === 'TEXTAREA')) {\n const candidate = el.tagName.toLowerCase() + '[name=\"' + safeCss(name) + '\"]';\n if (deepQueryAll(candidate).length === 1) return { selector: candidate, ambiguous: false };\n }\n // Structural fallback, verified like every shortcut above. The short\n // form (6 parts, indexed only where siblings force it) is preferred\n // for stability; when it matches more than one element across roots,\n // retry with a fully-qualified path (every level :nth-of-type-indexed,\n // climbed all the way to the root boundary).\n const structural = buildStructuralSelector(el, 6, false);\n try {\n if (deepQueryAll(structural).length === 1) return { selector: structural, ambiguous: false };\n } catch (_) {}\n const qualified = buildStructuralSelector(el, 32, true);\n try {\n if (deepQueryAll(qualified).length === 1) return { selector: qualified, ambiguous: false };\n } catch (_) {}\n // Still ambiguous: structurally-identical twins in different roots.\n // No CSS syntax can scope a selector to one shadow root / iframe, so\n // return the most qualified path and flag it \u2014 deepQueryFirst will\n // resolve it first-match-wins (deterministic traversal order), and\n // the flag tells agents not to cache it.\n return { selector: qualified, ambiguous: true };\n }\n function genSelector(el) {\n return genSelectorInfo(el).selector;\n }\n function buildStructuralSelector(el, maxParts, alwaysIndex) {\n // Local-root-scoped structural path: CLIMBING stops at a shadow root or\n // document boundary (parentElement is null for a shadow root's direct\n // children) \u2014 matches how deepQueryAll scopes each root's own\n // querySelectorAll call, so the path resolves back to the same element\n // when re-queried through deepQueryFirst. Sibling lookup for the\n // :nth-of-type qualifier uses parentNode rather than parentElement so a\n // shadow root's own top-level children (parentNode = the ShadowRoot,\n // which still exposes .children) get disambiguated too, even though we\n // do not climb past that boundary. With alwaysIndex, every level gets\n // an explicit :nth-of-type \u2014 maximum qualification for the retry pass.\n const parts = [];\n let cur = el;\n while (cur && cur.nodeType === 1 && cur !== document.body && parts.length < maxParts) {\n let part = cur.tagName.toLowerCase();\n const parentNode = cur.parentNode;\n if (parentNode && parentNode.children) {\n const sib = Array.from(parentNode.children).filter(s => s.tagName === cur.tagName);\n if (sib.length > 1 || alwaysIndex) {\n part += ':nth-of-type(' + (sib.indexOf(cur) + 1) + ')';\n }\n }\n parts.unshift(part);\n cur = cur.parentElement;\n }\n return parts.join(' > ');\n }\n function accessibleText(el) {\n const aria = el.getAttribute && el.getAttribute('aria-label');\n if (aria) return aria;\n const txt = (el.innerText || el.textContent || '').trim();\n if (txt) return txt;\n if ('value' in el && el.value) return String(el.value);\n const ph = el.getAttribute && el.getAttribute('placeholder');\n if (ph) return ph;\n const title = el.getAttribute && el.getAttribute('title');\n if (title) return title;\n return '';\n }\n function frameSelectorFor(el) {\n // Lightweight context field for snapshot/find entries: the selector of\n // the innermost same-origin <iframe> hosting el, or null when el lives\n // in the top document. Computed via genSelector so it benefits from the\n // same deep-uniqueness verification.\n if (el.ownerDocument === document) return null;\n const win = el.ownerDocument && el.ownerDocument.defaultView;\n let fe = null;\n try {\n fe = win && win.frameElement;\n } catch (_) {\n fe = null;\n }\n if (!fe) return null;\n return genSelector(fe);\n }\n";
|
|
@@ -32,6 +32,22 @@ export const DOM_HELPERS_JS = `
|
|
|
32
32
|
if (el.offsetParent === null && style.position !== 'fixed') return false;
|
|
33
33
|
return true;
|
|
34
34
|
}
|
|
35
|
+
function isClickTarget(el) {
|
|
36
|
+
// A computed pointer-events:none element can never be the browser's
|
|
37
|
+
// hit-test result — frameworks with an invisible accessibility layer
|
|
38
|
+
// (e.g. Articulate Storyline's #acc-* elements) stack such elements
|
|
39
|
+
// over the real hit-target sibling, so advertising one produces
|
|
40
|
+
// clicks that report success but land elsewhere. Per-element computed
|
|
41
|
+
// check, NOT a subtree prune: a descendant can re-enable itself with
|
|
42
|
+
// pointer-events: auto and IS a valid target. Only the interactive
|
|
43
|
+
// collections use this gate — headings/text keep plain isVisible.
|
|
44
|
+
if (!isVisible(el)) return false;
|
|
45
|
+
try {
|
|
46
|
+
const view = (el.ownerDocument && el.ownerDocument.defaultView) || window;
|
|
47
|
+
if (view.getComputedStyle(el).pointerEvents === 'none') return false;
|
|
48
|
+
} catch (_) {}
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
35
51
|
function shortText(el) {
|
|
36
52
|
const t = (el.innerText || el.textContent || '').trim();
|
|
37
53
|
return t.length > 120 ? t.slice(0, 120) + '...' : t;
|