ai-dev-browser 0.5.3__py3-none-any.whl
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.
- ai_dev_browser/__init__.py +110 -0
- ai_dev_browser/_cli.py +428 -0
- ai_dev_browser/_version.py +13 -0
- ai_dev_browser/cdp/__init__.py +6 -0
- ai_dev_browser/cdp/accessibility.py +668 -0
- ai_dev_browser/cdp/animation.py +494 -0
- ai_dev_browser/cdp/audits.py +1990 -0
- ai_dev_browser/cdp/autofill.py +292 -0
- ai_dev_browser/cdp/background_service.py +215 -0
- ai_dev_browser/cdp/bluetooth_emulation.py +626 -0
- ai_dev_browser/cdp/browser.py +821 -0
- ai_dev_browser/cdp/cache_storage.py +311 -0
- ai_dev_browser/cdp/cast.py +172 -0
- ai_dev_browser/cdp/console.py +107 -0
- ai_dev_browser/cdp/crash_report_context.py +55 -0
- ai_dev_browser/cdp/css.py +2710 -0
- ai_dev_browser/cdp/debugger.py +1405 -0
- ai_dev_browser/cdp/device_access.py +141 -0
- ai_dev_browser/cdp/device_orientation.py +45 -0
- ai_dev_browser/cdp/dom.py +2257 -0
- ai_dev_browser/cdp/dom_debugger.py +321 -0
- ai_dev_browser/cdp/dom_snapshot.py +876 -0
- ai_dev_browser/cdp/dom_storage.py +222 -0
- ai_dev_browser/cdp/emulation.py +1779 -0
- ai_dev_browser/cdp/event_breakpoints.py +56 -0
- ai_dev_browser/cdp/extensions.py +246 -0
- ai_dev_browser/cdp/fed_cm.py +283 -0
- ai_dev_browser/cdp/fetch.py +507 -0
- ai_dev_browser/cdp/file_system.py +115 -0
- ai_dev_browser/cdp/headless_experimental.py +115 -0
- ai_dev_browser/cdp/heap_profiler.py +401 -0
- ai_dev_browser/cdp/indexed_db.py +528 -0
- ai_dev_browser/cdp/input_.py +701 -0
- ai_dev_browser/cdp/inspector.py +95 -0
- ai_dev_browser/cdp/io.py +101 -0
- ai_dev_browser/cdp/layer_tree.py +464 -0
- ai_dev_browser/cdp/log.py +190 -0
- ai_dev_browser/cdp/media.py +313 -0
- ai_dev_browser/cdp/memory.py +305 -0
- ai_dev_browser/cdp/network.py +5317 -0
- ai_dev_browser/cdp/overlay.py +1468 -0
- ai_dev_browser/cdp/page.py +3970 -0
- ai_dev_browser/cdp/performance.py +124 -0
- ai_dev_browser/cdp/performance_timeline.py +200 -0
- ai_dev_browser/cdp/preload.py +575 -0
- ai_dev_browser/cdp/profiler.py +420 -0
- ai_dev_browser/cdp/pwa.py +278 -0
- ai_dev_browser/cdp/py.typed +0 -0
- ai_dev_browser/cdp/runtime.py +1589 -0
- ai_dev_browser/cdp/schema.py +50 -0
- ai_dev_browser/cdp/security.py +518 -0
- ai_dev_browser/cdp/service_worker.py +401 -0
- ai_dev_browser/cdp/smart_card_emulation.py +891 -0
- ai_dev_browser/cdp/storage.py +1573 -0
- ai_dev_browser/cdp/system_info.py +327 -0
- ai_dev_browser/cdp/target.py +822 -0
- ai_dev_browser/cdp/tethering.py +65 -0
- ai_dev_browser/cdp/tracing.py +377 -0
- ai_dev_browser/cdp/util.py +17 -0
- ai_dev_browser/cdp/web_audio.py +606 -0
- ai_dev_browser/cdp/web_authn.py +598 -0
- ai_dev_browser/cdp/web_mcp.py +219 -0
- ai_dev_browser/core/__init__.py +218 -0
- ai_dev_browser/core/_case.py +38 -0
- ai_dev_browser/core/_cf_template.py +84 -0
- ai_dev_browser/core/_element.py +431 -0
- ai_dev_browser/core/_tab.py +817 -0
- ai_dev_browser/core/_transport.py +362 -0
- ai_dev_browser/core/ax.py +605 -0
- ai_dev_browser/core/browser.py +323 -0
- ai_dev_browser/core/cdp.py +66 -0
- ai_dev_browser/core/chrome.py +253 -0
- ai_dev_browser/core/cloudflare.py +77 -0
- ai_dev_browser/core/config.py +95 -0
- ai_dev_browser/core/connection.py +403 -0
- ai_dev_browser/core/cookies.py +103 -0
- ai_dev_browser/core/dialog.py +165 -0
- ai_dev_browser/core/download.py +38 -0
- ai_dev_browser/core/elements.py +913 -0
- ai_dev_browser/core/human.py +612 -0
- ai_dev_browser/core/login.py +122 -0
- ai_dev_browser/core/mouse.py +140 -0
- ai_dev_browser/core/navigation.py +225 -0
- ai_dev_browser/core/overlays.py +148 -0
- ai_dev_browser/core/page.py +302 -0
- ai_dev_browser/core/port.py +465 -0
- ai_dev_browser/core/process.py +114 -0
- ai_dev_browser/core/snapshot.py +425 -0
- ai_dev_browser/core/storage.py +50 -0
- ai_dev_browser/core/tabs.py +125 -0
- ai_dev_browser/core/text_match.py +217 -0
- ai_dev_browser/core/window.py +84 -0
- ai_dev_browser/pool/__init__.py +34 -0
- ai_dev_browser/pool/job.py +140 -0
- ai_dev_browser/pool/persistence.py +154 -0
- ai_dev_browser/pool/pool.py +873 -0
- ai_dev_browser/pool/worker.py +171 -0
- ai_dev_browser/profile.py +107 -0
- ai_dev_browser/py.typed +0 -0
- ai_dev_browser/tools/__init__.py +9 -0
- ai_dev_browser/tools/_generate.py +209 -0
- ai_dev_browser/tools/browser_list.py +14 -0
- ai_dev_browser/tools/browser_start.py +14 -0
- ai_dev_browser/tools/browser_stop.py +14 -0
- ai_dev_browser/tools/cdp_send.py +14 -0
- ai_dev_browser/tools/click_by_html_id.py +14 -0
- ai_dev_browser/tools/click_by_ref.py +14 -0
- ai_dev_browser/tools/click_by_text.py +14 -0
- ai_dev_browser/tools/click_by_xpath.py +14 -0
- ai_dev_browser/tools/cloudflare_verify.py +14 -0
- ai_dev_browser/tools/cookies_list.py +14 -0
- ai_dev_browser/tools/cookies_load.py +14 -0
- ai_dev_browser/tools/cookies_save.py +14 -0
- ai_dev_browser/tools/dialog_respond.py +14 -0
- ai_dev_browser/tools/download.py +14 -0
- ai_dev_browser/tools/drag_by_ref.py +14 -0
- ai_dev_browser/tools/find_by_html_id.py +14 -0
- ai_dev_browser/tools/find_by_xpath.py +14 -0
- ai_dev_browser/tools/focus_by_ref.py +14 -0
- ai_dev_browser/tools/highlight_by_ref.py +14 -0
- ai_dev_browser/tools/hover_by_ref.py +14 -0
- ai_dev_browser/tools/html_by_ref.py +14 -0
- ai_dev_browser/tools/js_evaluate.py +14 -0
- ai_dev_browser/tools/login_interactive.py +14 -0
- ai_dev_browser/tools/mouse_click.py +14 -0
- ai_dev_browser/tools/mouse_drag.py +14 -0
- ai_dev_browser/tools/mouse_move.py +14 -0
- ai_dev_browser/tools/page_discover.py +14 -0
- ai_dev_browser/tools/page_emulate_focus.py +14 -0
- ai_dev_browser/tools/page_goto.py +14 -0
- ai_dev_browser/tools/page_html.py +14 -0
- ai_dev_browser/tools/page_info.py +14 -0
- ai_dev_browser/tools/page_reload.py +14 -0
- ai_dev_browser/tools/page_screenshot.py +14 -0
- ai_dev_browser/tools/page_scroll.py +14 -0
- ai_dev_browser/tools/page_wait_element.py +14 -0
- ai_dev_browser/tools/page_wait_ready.py +14 -0
- ai_dev_browser/tools/page_wait_url.py +14 -0
- ai_dev_browser/tools/screenshot_by_ref.py +14 -0
- ai_dev_browser/tools/select_by_ref.py +14 -0
- ai_dev_browser/tools/storage_get.py +14 -0
- ai_dev_browser/tools/storage_set.py +14 -0
- ai_dev_browser/tools/tab_close.py +14 -0
- ai_dev_browser/tools/tab_list.py +14 -0
- ai_dev_browser/tools/tab_new.py +14 -0
- ai_dev_browser/tools/tab_switch.py +14 -0
- ai_dev_browser/tools/type_by_ref.py +14 -0
- ai_dev_browser/tools/type_by_text.py +14 -0
- ai_dev_browser/tools/upload_by_ref.py +14 -0
- ai_dev_browser/tools/window_set.py +14 -0
- ai_dev_browser-0.5.3.dist-info/METADATA +177 -0
- ai_dev_browser-0.5.3.dist-info/RECORD +155 -0
- ai_dev_browser-0.5.3.dist-info/WHEEL +5 -0
- ai_dev_browser-0.5.3.dist-info/licenses/LICENSE +25 -0
- ai_dev_browser-0.5.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,605 @@
|
|
|
1
|
+
"""Accessibility tree operations for element interaction."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import contextlib
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
from ai_dev_browser.cdp import dom
|
|
8
|
+
from ai_dev_browser.cdp import input_ as cdp_input
|
|
9
|
+
from ai_dev_browser.cdp import page
|
|
10
|
+
|
|
11
|
+
from ._tab import Tab
|
|
12
|
+
|
|
13
|
+
from .snapshot import _get_snapshot
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _parse_ref(ref: str) -> tuple[str | None, str, int | None]:
|
|
17
|
+
"""Parse ref to extract frame prefix, local ref, and embedded node_id.
|
|
18
|
+
|
|
19
|
+
Ref format: "index#nodeId" or "FRAME_xxx:index#nodeId"
|
|
20
|
+
Examples:
|
|
21
|
+
"9#214" -> (None, "9", 214)
|
|
22
|
+
"FRAME_ABC123:9#214" -> ("FRAME_ABC123", "9", 214)
|
|
23
|
+
"9" -> (None, "9", None) # legacy format without node_id
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
(frame_id_prefix, local_ref, node_id)
|
|
27
|
+
"""
|
|
28
|
+
frame_prefix = None
|
|
29
|
+
local_ref = ref
|
|
30
|
+
node_id = None
|
|
31
|
+
|
|
32
|
+
# Check for frame prefix
|
|
33
|
+
frame_match = re.match(r"^(FRAME_[^:]+):(.+)$", ref)
|
|
34
|
+
if frame_match:
|
|
35
|
+
frame_prefix = frame_match.group(1)
|
|
36
|
+
local_ref = frame_match.group(2)
|
|
37
|
+
|
|
38
|
+
# Check for embedded node_id
|
|
39
|
+
node_match = re.match(r"^(\d+)#(\d+)$", local_ref)
|
|
40
|
+
if node_match:
|
|
41
|
+
local_ref = node_match.group(1)
|
|
42
|
+
node_id = int(node_match.group(2))
|
|
43
|
+
|
|
44
|
+
return frame_prefix, local_ref, node_id
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
async def _get_frame_id_by_prefix(tab: Tab, prefix: str) -> str | None:
|
|
48
|
+
"""Find full frame ID by prefix (e.g., 'FRAME_ABC123' -> full frame ID)."""
|
|
49
|
+
try:
|
|
50
|
+
result = await tab.send(page.get_frame_tree())
|
|
51
|
+
|
|
52
|
+
def find_frame(frame_tree):
|
|
53
|
+
frame = frame_tree.frame
|
|
54
|
+
if f"FRAME_{frame.id_[:8]}" == prefix:
|
|
55
|
+
return frame.id_
|
|
56
|
+
if frame_tree.child_frames:
|
|
57
|
+
for child in frame_tree.child_frames:
|
|
58
|
+
found = find_frame(child)
|
|
59
|
+
if found:
|
|
60
|
+
return found
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
return find_frame(result)
|
|
64
|
+
except Exception:
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
async def _click_by_node_id(
|
|
69
|
+
tab: Tab,
|
|
70
|
+
node_id: int,
|
|
71
|
+
) -> dict:
|
|
72
|
+
"""Click element by backend node ID via CDP.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
tab: Tab instance
|
|
76
|
+
node_id: Backend DOM node ID
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
dict with clicked status
|
|
80
|
+
"""
|
|
81
|
+
try:
|
|
82
|
+
# Wrap int in BackendNodeId
|
|
83
|
+
backend_node_id = dom.BackendNodeId(node_id)
|
|
84
|
+
|
|
85
|
+
# Get box model for the node
|
|
86
|
+
box = await tab.send(dom.get_box_model(backend_node_id=backend_node_id))
|
|
87
|
+
if not box or not box.content:
|
|
88
|
+
return {"clicked": False, "error": "Could not get element box model"}
|
|
89
|
+
|
|
90
|
+
# Get center of content box (content quad has 8 values: 4 x,y pairs)
|
|
91
|
+
quad = box.content
|
|
92
|
+
x = (quad[0] + quad[2] + quad[4] + quad[6]) / 4
|
|
93
|
+
y = (quad[1] + quad[3] + quad[5] + quad[7]) / 4
|
|
94
|
+
|
|
95
|
+
# Dispatch mouse events
|
|
96
|
+
await tab.send(
|
|
97
|
+
cdp_input.dispatch_mouse_event(
|
|
98
|
+
type_="mousePressed",
|
|
99
|
+
x=x,
|
|
100
|
+
y=y,
|
|
101
|
+
button=cdp_input.MouseButton.LEFT,
|
|
102
|
+
click_count=1,
|
|
103
|
+
)
|
|
104
|
+
)
|
|
105
|
+
await tab.send(
|
|
106
|
+
cdp_input.dispatch_mouse_event(
|
|
107
|
+
type_="mouseReleased",
|
|
108
|
+
x=x,
|
|
109
|
+
y=y,
|
|
110
|
+
button=cdp_input.MouseButton.LEFT,
|
|
111
|
+
click_count=1,
|
|
112
|
+
)
|
|
113
|
+
)
|
|
114
|
+
return {"clicked": True, "node_id": node_id}
|
|
115
|
+
except Exception as e:
|
|
116
|
+
return {"clicked": False, "error": str(e)}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
async def _wait_for_ax_element(
|
|
120
|
+
tab: Tab,
|
|
121
|
+
wait_for_role: str | None = None,
|
|
122
|
+
wait_for_name: str | None = None,
|
|
123
|
+
timeout: float = 5.0,
|
|
124
|
+
interval: float = 0.3,
|
|
125
|
+
) -> dict:
|
|
126
|
+
"""Wait for an element to appear in the accessibility tree.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
tab: Tab instance
|
|
130
|
+
wait_for_role: Role to wait for (e.g., "button", "menu")
|
|
131
|
+
wait_for_name: Name to wait for (substring match)
|
|
132
|
+
timeout: Max wait time in seconds
|
|
133
|
+
interval: Poll interval in seconds
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
dict with found status and element info
|
|
137
|
+
"""
|
|
138
|
+
if not wait_for_role and not wait_for_name:
|
|
139
|
+
return {"found": True, "skipped": True}
|
|
140
|
+
|
|
141
|
+
elapsed = 0.0
|
|
142
|
+
while elapsed < timeout:
|
|
143
|
+
await asyncio.sleep(interval)
|
|
144
|
+
elapsed += interval
|
|
145
|
+
|
|
146
|
+
try:
|
|
147
|
+
elements = await _get_snapshot(tab)
|
|
148
|
+
for el in elements:
|
|
149
|
+
role_match = wait_for_role is None or el.get("role") == wait_for_role
|
|
150
|
+
name_match = wait_for_name is None or wait_for_name in el.get(
|
|
151
|
+
"name", ""
|
|
152
|
+
)
|
|
153
|
+
if role_match and name_match:
|
|
154
|
+
return {
|
|
155
|
+
"found": True,
|
|
156
|
+
"element": {
|
|
157
|
+
"role": el.get("role"),
|
|
158
|
+
"name": el.get("name"),
|
|
159
|
+
"ref": el.get("ref"),
|
|
160
|
+
},
|
|
161
|
+
}
|
|
162
|
+
except Exception:
|
|
163
|
+
pass # Keep trying
|
|
164
|
+
|
|
165
|
+
return {"found": False, "timeout": True}
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
async def _click_ax_element(
|
|
169
|
+
tab: Tab,
|
|
170
|
+
ref: str | None = None,
|
|
171
|
+
node_id: int | None = None,
|
|
172
|
+
wait_for_role: str | None = None,
|
|
173
|
+
wait_for_name: str | None = None,
|
|
174
|
+
wait_timeout: float = 5.0,
|
|
175
|
+
wait_interval: float = 0.3,
|
|
176
|
+
) -> dict:
|
|
177
|
+
"""Click element by accessibility tree ref or node_id.
|
|
178
|
+
|
|
179
|
+
Use ax_tree (get_accessibility_tree) to get element refs, then this function
|
|
180
|
+
to click them. For stable clicks, pass node_id directly.
|
|
181
|
+
|
|
182
|
+
Supports iframe elements with prefixed refs like "FRAME_ABC123:5".
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
tab: Tab instance
|
|
186
|
+
ref: Element ref from ax_tree (e.g., "5" or "FRAME_ABC123:5" or "5#214")
|
|
187
|
+
node_id: Backend node ID - direct click, no re-fetch needed
|
|
188
|
+
wait_for_role: After click, wait for element with this role
|
|
189
|
+
wait_for_name: After click, wait for element with this name
|
|
190
|
+
wait_timeout: Max time to wait in seconds
|
|
191
|
+
wait_interval: Poll interval in seconds
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
dict with clicked status, element info, and optional wait result
|
|
195
|
+
"""
|
|
196
|
+
# Must specify at least one of ref or node_id
|
|
197
|
+
if ref is None and node_id is None:
|
|
198
|
+
return {"error": "Must specify ref or node_id"}
|
|
199
|
+
|
|
200
|
+
# If node_id provided directly, use it (stable, no re-fetch)
|
|
201
|
+
if node_id is not None:
|
|
202
|
+
result = await _click_by_node_id(tab, node_id)
|
|
203
|
+
if result.get("clicked") and (wait_for_role or wait_for_name):
|
|
204
|
+
waited = await _wait_for_ax_element(
|
|
205
|
+
tab, wait_for_role, wait_for_name, wait_timeout, wait_interval
|
|
206
|
+
)
|
|
207
|
+
if waited.get("found") and not waited.get("skipped"):
|
|
208
|
+
result["waited_for"] = waited.get("element")
|
|
209
|
+
elif waited.get("timeout"):
|
|
210
|
+
result["wait_timeout"] = True
|
|
211
|
+
return result
|
|
212
|
+
|
|
213
|
+
# Parse ref to extract frame prefix, local ref, and embedded node_id
|
|
214
|
+
frame_prefix, local_ref, embedded_node_id = _parse_ref(ref)
|
|
215
|
+
|
|
216
|
+
# If ref contains embedded node_id, use it directly (most reliable)
|
|
217
|
+
if embedded_node_id is not None:
|
|
218
|
+
result = await _click_by_node_id(tab, embedded_node_id)
|
|
219
|
+
if result.get("clicked"):
|
|
220
|
+
result["ref"] = ref
|
|
221
|
+
if wait_for_role or wait_for_name:
|
|
222
|
+
waited = await _wait_for_ax_element(
|
|
223
|
+
tab, wait_for_role, wait_for_name, wait_timeout, wait_interval
|
|
224
|
+
)
|
|
225
|
+
if waited.get("found") and not waited.get("skipped"):
|
|
226
|
+
result["waited_for"] = waited.get("element")
|
|
227
|
+
elif waited.get("timeout"):
|
|
228
|
+
result["wait_timeout"] = True
|
|
229
|
+
return result
|
|
230
|
+
|
|
231
|
+
# Fallback: re-fetch snapshot and find by ref (less reliable)
|
|
232
|
+
# Get frame ID if this is an iframe ref
|
|
233
|
+
frame_id = None
|
|
234
|
+
if frame_prefix:
|
|
235
|
+
frame_id = await _get_frame_id_by_prefix(tab, frame_prefix)
|
|
236
|
+
if not frame_id:
|
|
237
|
+
return {"error": f"Frame '{frame_prefix}' not found"}
|
|
238
|
+
|
|
239
|
+
# Get accessibility tree for the appropriate frame
|
|
240
|
+
elements = await _get_snapshot(tab, frame_id=frame_id)
|
|
241
|
+
|
|
242
|
+
# Find element by local ref (without frame prefix or node_id suffix)
|
|
243
|
+
target = None
|
|
244
|
+
for el in elements:
|
|
245
|
+
# Match by index part only (el.ref might be "9#214", we want to match "9")
|
|
246
|
+
el_ref = el.get("ref", "")
|
|
247
|
+
el_index = el_ref.split("#")[0] if "#" in el_ref else el_ref
|
|
248
|
+
if el_index == local_ref:
|
|
249
|
+
target = el
|
|
250
|
+
break
|
|
251
|
+
|
|
252
|
+
if not target:
|
|
253
|
+
return {"error": f"Element with ref '{ref}' not found"}
|
|
254
|
+
|
|
255
|
+
# Extract node_id from target's ref
|
|
256
|
+
target_ref = target.get("ref", "")
|
|
257
|
+
target_node_id = None
|
|
258
|
+
if "#" in target_ref:
|
|
259
|
+
with contextlib.suppress(ValueError):
|
|
260
|
+
target_node_id = int(target_ref.split("#")[1])
|
|
261
|
+
|
|
262
|
+
if not target_node_id:
|
|
263
|
+
return {"error": f"Element ref '{ref}' has no nodeId"}
|
|
264
|
+
|
|
265
|
+
# Click the element
|
|
266
|
+
result = await _click_by_node_id(tab, target_node_id)
|
|
267
|
+
if result.get("clicked"):
|
|
268
|
+
result["ref"] = ref
|
|
269
|
+
result["element"] = {
|
|
270
|
+
"role": target.get("role"),
|
|
271
|
+
"name": target.get("name"),
|
|
272
|
+
}
|
|
273
|
+
if wait_for_role or wait_for_name:
|
|
274
|
+
waited = await _wait_for_ax_element(
|
|
275
|
+
tab, wait_for_role, wait_for_name, wait_timeout, wait_interval
|
|
276
|
+
)
|
|
277
|
+
if waited.get("found") and not waited.get("skipped"):
|
|
278
|
+
result["waited_for"] = waited.get("element")
|
|
279
|
+
elif waited.get("timeout"):
|
|
280
|
+
result["wait_timeout"] = True
|
|
281
|
+
|
|
282
|
+
return result
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
async def click_by_ref(
|
|
286
|
+
tab: Tab,
|
|
287
|
+
ref: str,
|
|
288
|
+
) -> dict:
|
|
289
|
+
"""Click element by ref from page_discover().
|
|
290
|
+
|
|
291
|
+
This is the primary way for AI to click elements by ref. Use page_discover() first
|
|
292
|
+
to get element refs, then click_by_ref with the ref string.
|
|
293
|
+
|
|
294
|
+
Args:
|
|
295
|
+
tab: Tab instance
|
|
296
|
+
ref: Element ref from page_discover() (e.g., "5#214" or "FRAME_ABC123:5#214")
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
dict with clicked status, element info, and navigation feedback:
|
|
300
|
+
`{clicked, ref, role, name, url_before, url_after, title_after, navigated}`.
|
|
301
|
+
`navigated=True` means the top-level URL changed after the click
|
|
302
|
+
(SPA route change or full page load). Use this to confirm the click
|
|
303
|
+
had the intended side effect instead of chaining a screenshot + discover.
|
|
304
|
+
|
|
305
|
+
Example:
|
|
306
|
+
# First page_discover elements
|
|
307
|
+
result = page_discover()
|
|
308
|
+
# Then click by ref
|
|
309
|
+
click_by_ref("5#214")
|
|
310
|
+
"""
|
|
311
|
+
# Import lazily to avoid a circular dependency with elements.py, which
|
|
312
|
+
# imports from this module.
|
|
313
|
+
from .elements import _capture_page_state, _with_nav_feedback
|
|
314
|
+
|
|
315
|
+
url_before_state = await _capture_page_state(tab)
|
|
316
|
+
result = await _click_ax_element(tab, ref=ref)
|
|
317
|
+
result["url_before"] = url_before_state.get("url", "")
|
|
318
|
+
if not result.get("clicked"):
|
|
319
|
+
result.update(
|
|
320
|
+
{
|
|
321
|
+
"navigated": False,
|
|
322
|
+
"url_after": result["url_before"],
|
|
323
|
+
"title_after": "",
|
|
324
|
+
}
|
|
325
|
+
)
|
|
326
|
+
return result
|
|
327
|
+
return await _with_nav_feedback(tab, result)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
async def focus_by_ref(
|
|
331
|
+
tab: Tab,
|
|
332
|
+
ref: str,
|
|
333
|
+
) -> dict:
|
|
334
|
+
"""Focus element by ref from page_discover().
|
|
335
|
+
|
|
336
|
+
Use page_discover() first to get element refs, then focus_by_ref.
|
|
337
|
+
Useful when you need to focus without clicking (e.g., to avoid
|
|
338
|
+
triggering click handlers).
|
|
339
|
+
|
|
340
|
+
Args:
|
|
341
|
+
tab: Tab instance
|
|
342
|
+
ref: Element ref from page_discover() (e.g., "5#214")
|
|
343
|
+
|
|
344
|
+
Returns:
|
|
345
|
+
dict with focused status
|
|
346
|
+
|
|
347
|
+
Example:
|
|
348
|
+
focus_by_ref("5#214")
|
|
349
|
+
"""
|
|
350
|
+
# Parse ref to extract node_id
|
|
351
|
+
_, _, node_id = _parse_ref(ref)
|
|
352
|
+
|
|
353
|
+
if node_id is None:
|
|
354
|
+
return {"focused": False, "error": f"Invalid ref format: {ref}"}
|
|
355
|
+
|
|
356
|
+
try:
|
|
357
|
+
backend_node_id = dom.BackendNodeId(node_id)
|
|
358
|
+
await tab.send(dom.focus(backend_node_id=backend_node_id))
|
|
359
|
+
return {"focused": True, "ref": ref}
|
|
360
|
+
except Exception as e:
|
|
361
|
+
return {"focused": False, "error": str(e)}
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
async def type_by_ref(
|
|
365
|
+
tab: Tab,
|
|
366
|
+
ref: str,
|
|
367
|
+
text: str,
|
|
368
|
+
clear: bool = False,
|
|
369
|
+
) -> dict:
|
|
370
|
+
"""Type text into element by ref from page_discover().
|
|
371
|
+
|
|
372
|
+
Use page_discover() first to get element refs, then type_by_ref.
|
|
373
|
+
|
|
374
|
+
Args:
|
|
375
|
+
tab: Tab instance
|
|
376
|
+
ref: Element ref from page_discover() (e.g., "5#214")
|
|
377
|
+
text: Text to type into the element
|
|
378
|
+
clear: If True, clear existing content first
|
|
379
|
+
|
|
380
|
+
Returns:
|
|
381
|
+
dict with typed status
|
|
382
|
+
|
|
383
|
+
Example:
|
|
384
|
+
type_by_ref("5#214", "myusername")
|
|
385
|
+
type_by_ref("5#214", "newvalue", clear=True)
|
|
386
|
+
"""
|
|
387
|
+
# First focus the element
|
|
388
|
+
focus_result = await focus_by_ref(tab, ref)
|
|
389
|
+
if not focus_result.get("focused"):
|
|
390
|
+
return {"typed": False, "error": focus_result.get("error", "Focus failed")}
|
|
391
|
+
|
|
392
|
+
# Clear if requested (select all + delete)
|
|
393
|
+
if clear:
|
|
394
|
+
# Select all
|
|
395
|
+
await tab.send(
|
|
396
|
+
cdp_input.dispatch_key_event(
|
|
397
|
+
type_="keyDown",
|
|
398
|
+
modifiers=2, # Ctrl/Cmd
|
|
399
|
+
key="a",
|
|
400
|
+
code="KeyA",
|
|
401
|
+
)
|
|
402
|
+
)
|
|
403
|
+
await tab.send(
|
|
404
|
+
cdp_input.dispatch_key_event(
|
|
405
|
+
type_="keyUp",
|
|
406
|
+
modifiers=2,
|
|
407
|
+
key="a",
|
|
408
|
+
code="KeyA",
|
|
409
|
+
)
|
|
410
|
+
)
|
|
411
|
+
# Delete
|
|
412
|
+
await tab.send(
|
|
413
|
+
cdp_input.dispatch_key_event(
|
|
414
|
+
type_="keyDown",
|
|
415
|
+
key="Backspace",
|
|
416
|
+
code="Backspace",
|
|
417
|
+
)
|
|
418
|
+
)
|
|
419
|
+
await tab.send(
|
|
420
|
+
cdp_input.dispatch_key_event(
|
|
421
|
+
type_="keyUp",
|
|
422
|
+
key="Backspace",
|
|
423
|
+
code="Backspace",
|
|
424
|
+
)
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
# Type text using insertText (most reliable for input fields)
|
|
428
|
+
await tab.send(cdp_input.insert_text(text=text))
|
|
429
|
+
|
|
430
|
+
return {"typed": True, "ref": ref, "text": text}
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
# ---------------------------------------------------------------------------
|
|
434
|
+
# Element tools (by ref) — all use get_element_by_ref helper
|
|
435
|
+
# ---------------------------------------------------------------------------
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
async def hover_by_ref(
|
|
439
|
+
tab: Tab,
|
|
440
|
+
ref: str,
|
|
441
|
+
) -> dict:
|
|
442
|
+
"""Move mouse to element (hover) by ref from page_discover().
|
|
443
|
+
|
|
444
|
+
Useful for triggering hover menus, tooltips, or dropdown previews.
|
|
445
|
+
|
|
446
|
+
Args:
|
|
447
|
+
tab: Tab instance
|
|
448
|
+
ref: Element ref from page_discover()
|
|
449
|
+
|
|
450
|
+
Returns:
|
|
451
|
+
dict with hovered status
|
|
452
|
+
"""
|
|
453
|
+
from ._element import get_element_by_ref
|
|
454
|
+
|
|
455
|
+
element = await get_element_by_ref(tab, ref)
|
|
456
|
+
await element.mouse_move()
|
|
457
|
+
return {"hovered": True, "ref": ref}
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
async def highlight_by_ref(
|
|
461
|
+
tab: Tab,
|
|
462
|
+
ref: str,
|
|
463
|
+
duration: float = 2.0,
|
|
464
|
+
) -> dict:
|
|
465
|
+
"""Highlight element with colored overlay by ref from page_discover().
|
|
466
|
+
|
|
467
|
+
Useful for visual debugging — confirms which element was found.
|
|
468
|
+
|
|
469
|
+
Args:
|
|
470
|
+
tab: Tab instance
|
|
471
|
+
ref: Element ref from page_discover()
|
|
472
|
+
duration: How long to show highlight in seconds
|
|
473
|
+
|
|
474
|
+
Returns:
|
|
475
|
+
dict with highlighted status
|
|
476
|
+
"""
|
|
477
|
+
from ._element import get_element_by_ref
|
|
478
|
+
|
|
479
|
+
element = await get_element_by_ref(tab, ref)
|
|
480
|
+
await element.highlight_overlay(duration=duration)
|
|
481
|
+
return {"highlighted": True, "ref": ref}
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
async def html_by_ref(
|
|
485
|
+
tab: Tab,
|
|
486
|
+
ref: str,
|
|
487
|
+
) -> dict:
|
|
488
|
+
"""Get outerHTML of element by ref from page_discover().
|
|
489
|
+
|
|
490
|
+
Args:
|
|
491
|
+
tab: Tab instance
|
|
492
|
+
ref: Element ref from page_discover()
|
|
493
|
+
|
|
494
|
+
Returns:
|
|
495
|
+
dict with html content
|
|
496
|
+
"""
|
|
497
|
+
from ._element import get_element_by_ref
|
|
498
|
+
|
|
499
|
+
element = await get_element_by_ref(tab, ref)
|
|
500
|
+
html = await element.get_html()
|
|
501
|
+
return {"html": html, "ref": ref}
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
async def screenshot_by_ref(
|
|
505
|
+
tab: Tab,
|
|
506
|
+
ref: str,
|
|
507
|
+
path: str | None = None,
|
|
508
|
+
) -> dict:
|
|
509
|
+
"""Take screenshot of just this element's region by ref from page_discover().
|
|
510
|
+
|
|
511
|
+
Args:
|
|
512
|
+
tab: Tab instance
|
|
513
|
+
ref: Element ref from page_discover()
|
|
514
|
+
path: File path to save (default: screenshots/{timestamp}_element.png)
|
|
515
|
+
|
|
516
|
+
Returns:
|
|
517
|
+
dict with path
|
|
518
|
+
"""
|
|
519
|
+
import datetime
|
|
520
|
+
from pathlib import Path
|
|
521
|
+
|
|
522
|
+
from ._element import get_element_by_ref
|
|
523
|
+
from .config import DEFAULT_SCREENSHOT_DIR
|
|
524
|
+
|
|
525
|
+
if path is None:
|
|
526
|
+
DEFAULT_SCREENSHOT_DIR.mkdir(parents=True, exist_ok=True)
|
|
527
|
+
ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S_%f")
|
|
528
|
+
path = str(DEFAULT_SCREENSHOT_DIR / f"{ts}_element.png")
|
|
529
|
+
|
|
530
|
+
element = await get_element_by_ref(tab, ref)
|
|
531
|
+
saved = await element.save_screenshot(path)
|
|
532
|
+
file_size = Path(saved).stat().st_size
|
|
533
|
+
return {"path": saved, "size": file_size, "ref": ref}
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
async def select_by_ref(
|
|
537
|
+
tab: Tab,
|
|
538
|
+
ref: str,
|
|
539
|
+
) -> dict:
|
|
540
|
+
"""Select a dropdown option by ref from page_discover().
|
|
541
|
+
|
|
542
|
+
For <select> elements: use page_discover() to see options, then select_by_ref
|
|
543
|
+
on the <option> element.
|
|
544
|
+
|
|
545
|
+
Args:
|
|
546
|
+
tab: Tab instance
|
|
547
|
+
ref: Element ref of the <option> to select
|
|
548
|
+
|
|
549
|
+
Returns:
|
|
550
|
+
dict with selected status
|
|
551
|
+
"""
|
|
552
|
+
from ._element import get_element_by_ref
|
|
553
|
+
|
|
554
|
+
element = await get_element_by_ref(tab, ref)
|
|
555
|
+
await element.select_option()
|
|
556
|
+
return {"selected": True, "ref": ref}
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
async def upload_by_ref(
|
|
560
|
+
tab: Tab,
|
|
561
|
+
ref: str,
|
|
562
|
+
paths: str,
|
|
563
|
+
) -> dict:
|
|
564
|
+
"""Upload file(s) to a file input by ref from page_discover().
|
|
565
|
+
|
|
566
|
+
Args:
|
|
567
|
+
tab: Tab instance
|
|
568
|
+
ref: Element ref of the <input type="file">
|
|
569
|
+
paths: Comma-separated file paths to upload
|
|
570
|
+
|
|
571
|
+
Returns:
|
|
572
|
+
dict with uploaded status and file count
|
|
573
|
+
"""
|
|
574
|
+
from ._element import get_element_by_ref
|
|
575
|
+
|
|
576
|
+
element = await get_element_by_ref(tab, ref)
|
|
577
|
+
file_list = [p.strip() for p in paths.split(",")]
|
|
578
|
+
await element.send_file(*file_list)
|
|
579
|
+
return {"uploaded": True, "ref": ref, "files": len(file_list)}
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
async def drag_by_ref(
|
|
583
|
+
tab: Tab,
|
|
584
|
+
ref: str,
|
|
585
|
+
to_x: float,
|
|
586
|
+
to_y: float,
|
|
587
|
+
steps: int = 10,
|
|
588
|
+
) -> dict:
|
|
589
|
+
"""Drag element to destination coordinates by ref from page_discover().
|
|
590
|
+
|
|
591
|
+
Args:
|
|
592
|
+
tab: Tab instance
|
|
593
|
+
ref: Element ref to drag from
|
|
594
|
+
to_x: Destination X coordinate
|
|
595
|
+
to_y: Destination Y coordinate
|
|
596
|
+
steps: Number of intermediate steps
|
|
597
|
+
|
|
598
|
+
Returns:
|
|
599
|
+
dict with dragged status
|
|
600
|
+
"""
|
|
601
|
+
from ._element import get_element_by_ref
|
|
602
|
+
|
|
603
|
+
element = await get_element_by_ref(tab, ref)
|
|
604
|
+
await element.mouse_drag(to_x, to_y, steps=steps)
|
|
605
|
+
return {"dragged": True, "ref": ref}
|