optexity-browser-use 0.9.5__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.
Files changed (147) hide show
  1. browser_use/__init__.py +157 -0
  2. browser_use/actor/__init__.py +11 -0
  3. browser_use/actor/element.py +1175 -0
  4. browser_use/actor/mouse.py +134 -0
  5. browser_use/actor/page.py +561 -0
  6. browser_use/actor/playground/flights.py +41 -0
  7. browser_use/actor/playground/mixed_automation.py +54 -0
  8. browser_use/actor/playground/playground.py +236 -0
  9. browser_use/actor/utils.py +176 -0
  10. browser_use/agent/cloud_events.py +282 -0
  11. browser_use/agent/gif.py +424 -0
  12. browser_use/agent/judge.py +170 -0
  13. browser_use/agent/message_manager/service.py +473 -0
  14. browser_use/agent/message_manager/utils.py +52 -0
  15. browser_use/agent/message_manager/views.py +98 -0
  16. browser_use/agent/prompts.py +413 -0
  17. browser_use/agent/service.py +2316 -0
  18. browser_use/agent/system_prompt.md +185 -0
  19. browser_use/agent/system_prompt_flash.md +10 -0
  20. browser_use/agent/system_prompt_no_thinking.md +183 -0
  21. browser_use/agent/views.py +743 -0
  22. browser_use/browser/__init__.py +41 -0
  23. browser_use/browser/cloud/cloud.py +203 -0
  24. browser_use/browser/cloud/views.py +89 -0
  25. browser_use/browser/events.py +578 -0
  26. browser_use/browser/profile.py +1158 -0
  27. browser_use/browser/python_highlights.py +548 -0
  28. browser_use/browser/session.py +3225 -0
  29. browser_use/browser/session_manager.py +399 -0
  30. browser_use/browser/video_recorder.py +162 -0
  31. browser_use/browser/views.py +200 -0
  32. browser_use/browser/watchdog_base.py +260 -0
  33. browser_use/browser/watchdogs/__init__.py +0 -0
  34. browser_use/browser/watchdogs/aboutblank_watchdog.py +253 -0
  35. browser_use/browser/watchdogs/crash_watchdog.py +335 -0
  36. browser_use/browser/watchdogs/default_action_watchdog.py +2729 -0
  37. browser_use/browser/watchdogs/dom_watchdog.py +817 -0
  38. browser_use/browser/watchdogs/downloads_watchdog.py +1277 -0
  39. browser_use/browser/watchdogs/local_browser_watchdog.py +461 -0
  40. browser_use/browser/watchdogs/permissions_watchdog.py +43 -0
  41. browser_use/browser/watchdogs/popups_watchdog.py +143 -0
  42. browser_use/browser/watchdogs/recording_watchdog.py +126 -0
  43. browser_use/browser/watchdogs/screenshot_watchdog.py +62 -0
  44. browser_use/browser/watchdogs/security_watchdog.py +280 -0
  45. browser_use/browser/watchdogs/storage_state_watchdog.py +335 -0
  46. browser_use/cli.py +2359 -0
  47. browser_use/code_use/__init__.py +16 -0
  48. browser_use/code_use/formatting.py +192 -0
  49. browser_use/code_use/namespace.py +665 -0
  50. browser_use/code_use/notebook_export.py +276 -0
  51. browser_use/code_use/service.py +1340 -0
  52. browser_use/code_use/system_prompt.md +574 -0
  53. browser_use/code_use/utils.py +150 -0
  54. browser_use/code_use/views.py +171 -0
  55. browser_use/config.py +505 -0
  56. browser_use/controller/__init__.py +3 -0
  57. browser_use/dom/enhanced_snapshot.py +161 -0
  58. browser_use/dom/markdown_extractor.py +169 -0
  59. browser_use/dom/playground/extraction.py +312 -0
  60. browser_use/dom/playground/multi_act.py +32 -0
  61. browser_use/dom/serializer/clickable_elements.py +200 -0
  62. browser_use/dom/serializer/code_use_serializer.py +287 -0
  63. browser_use/dom/serializer/eval_serializer.py +478 -0
  64. browser_use/dom/serializer/html_serializer.py +212 -0
  65. browser_use/dom/serializer/paint_order.py +197 -0
  66. browser_use/dom/serializer/serializer.py +1170 -0
  67. browser_use/dom/service.py +825 -0
  68. browser_use/dom/utils.py +129 -0
  69. browser_use/dom/views.py +906 -0
  70. browser_use/exceptions.py +5 -0
  71. browser_use/filesystem/__init__.py +0 -0
  72. browser_use/filesystem/file_system.py +619 -0
  73. browser_use/init_cmd.py +376 -0
  74. browser_use/integrations/gmail/__init__.py +24 -0
  75. browser_use/integrations/gmail/actions.py +115 -0
  76. browser_use/integrations/gmail/service.py +225 -0
  77. browser_use/llm/__init__.py +155 -0
  78. browser_use/llm/anthropic/chat.py +242 -0
  79. browser_use/llm/anthropic/serializer.py +312 -0
  80. browser_use/llm/aws/__init__.py +36 -0
  81. browser_use/llm/aws/chat_anthropic.py +242 -0
  82. browser_use/llm/aws/chat_bedrock.py +289 -0
  83. browser_use/llm/aws/serializer.py +257 -0
  84. browser_use/llm/azure/chat.py +91 -0
  85. browser_use/llm/base.py +57 -0
  86. browser_use/llm/browser_use/__init__.py +3 -0
  87. browser_use/llm/browser_use/chat.py +201 -0
  88. browser_use/llm/cerebras/chat.py +193 -0
  89. browser_use/llm/cerebras/serializer.py +109 -0
  90. browser_use/llm/deepseek/chat.py +212 -0
  91. browser_use/llm/deepseek/serializer.py +109 -0
  92. browser_use/llm/exceptions.py +29 -0
  93. browser_use/llm/google/__init__.py +3 -0
  94. browser_use/llm/google/chat.py +542 -0
  95. browser_use/llm/google/serializer.py +120 -0
  96. browser_use/llm/groq/chat.py +229 -0
  97. browser_use/llm/groq/parser.py +158 -0
  98. browser_use/llm/groq/serializer.py +159 -0
  99. browser_use/llm/messages.py +238 -0
  100. browser_use/llm/models.py +271 -0
  101. browser_use/llm/oci_raw/__init__.py +10 -0
  102. browser_use/llm/oci_raw/chat.py +443 -0
  103. browser_use/llm/oci_raw/serializer.py +229 -0
  104. browser_use/llm/ollama/chat.py +97 -0
  105. browser_use/llm/ollama/serializer.py +143 -0
  106. browser_use/llm/openai/chat.py +264 -0
  107. browser_use/llm/openai/like.py +15 -0
  108. browser_use/llm/openai/serializer.py +165 -0
  109. browser_use/llm/openrouter/chat.py +211 -0
  110. browser_use/llm/openrouter/serializer.py +26 -0
  111. browser_use/llm/schema.py +176 -0
  112. browser_use/llm/views.py +48 -0
  113. browser_use/logging_config.py +330 -0
  114. browser_use/mcp/__init__.py +18 -0
  115. browser_use/mcp/__main__.py +12 -0
  116. browser_use/mcp/client.py +544 -0
  117. browser_use/mcp/controller.py +264 -0
  118. browser_use/mcp/server.py +1114 -0
  119. browser_use/observability.py +204 -0
  120. browser_use/py.typed +0 -0
  121. browser_use/sandbox/__init__.py +41 -0
  122. browser_use/sandbox/sandbox.py +637 -0
  123. browser_use/sandbox/views.py +132 -0
  124. browser_use/screenshots/__init__.py +1 -0
  125. browser_use/screenshots/service.py +52 -0
  126. browser_use/sync/__init__.py +6 -0
  127. browser_use/sync/auth.py +357 -0
  128. browser_use/sync/service.py +161 -0
  129. browser_use/telemetry/__init__.py +51 -0
  130. browser_use/telemetry/service.py +112 -0
  131. browser_use/telemetry/views.py +101 -0
  132. browser_use/tokens/__init__.py +0 -0
  133. browser_use/tokens/custom_pricing.py +24 -0
  134. browser_use/tokens/mappings.py +4 -0
  135. browser_use/tokens/service.py +580 -0
  136. browser_use/tokens/views.py +108 -0
  137. browser_use/tools/registry/service.py +572 -0
  138. browser_use/tools/registry/views.py +174 -0
  139. browser_use/tools/service.py +1675 -0
  140. browser_use/tools/utils.py +82 -0
  141. browser_use/tools/views.py +100 -0
  142. browser_use/utils.py +670 -0
  143. optexity_browser_use-0.9.5.dist-info/METADATA +344 -0
  144. optexity_browser_use-0.9.5.dist-info/RECORD +147 -0
  145. optexity_browser_use-0.9.5.dist-info/WHEEL +4 -0
  146. optexity_browser_use-0.9.5.dist-info/entry_points.txt +3 -0
  147. optexity_browser_use-0.9.5.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,1175 @@
1
+ """Element class for element operations."""
2
+
3
+ import asyncio
4
+ from typing import TYPE_CHECKING, Literal, Union
5
+
6
+ from cdp_use.client import logger
7
+ from typing_extensions import TypedDict
8
+
9
+ if TYPE_CHECKING:
10
+ from cdp_use.cdp.dom.commands import (
11
+ DescribeNodeParameters,
12
+ FocusParameters,
13
+ GetAttributesParameters,
14
+ GetBoxModelParameters,
15
+ PushNodesByBackendIdsToFrontendParameters,
16
+ RequestChildNodesParameters,
17
+ ResolveNodeParameters,
18
+ )
19
+ from cdp_use.cdp.input.commands import (
20
+ DispatchMouseEventParameters,
21
+ )
22
+ from cdp_use.cdp.input.types import MouseButton
23
+ from cdp_use.cdp.page.commands import CaptureScreenshotParameters
24
+ from cdp_use.cdp.page.types import Viewport
25
+ from cdp_use.cdp.runtime.commands import CallFunctionOnParameters
26
+
27
+ from browser_use.browser.session import BrowserSession
28
+
29
+ # Type definitions for element operations
30
+ ModifierType = Literal['Alt', 'Control', 'Meta', 'Shift']
31
+
32
+
33
+ class Position(TypedDict):
34
+ """2D position coordinates."""
35
+
36
+ x: float
37
+ y: float
38
+
39
+
40
+ class BoundingBox(TypedDict):
41
+ """Element bounding box with position and dimensions."""
42
+
43
+ x: float
44
+ y: float
45
+ width: float
46
+ height: float
47
+
48
+
49
+ class ElementInfo(TypedDict):
50
+ """Basic information about a DOM element."""
51
+
52
+ backendNodeId: int
53
+ nodeId: int | None
54
+ nodeName: str
55
+ nodeType: int
56
+ nodeValue: str | None
57
+ attributes: dict[str, str]
58
+ boundingBox: BoundingBox | None
59
+ error: str | None
60
+
61
+
62
+ class Element:
63
+ """Element operations using BackendNodeId."""
64
+
65
+ def __init__(
66
+ self,
67
+ browser_session: 'BrowserSession',
68
+ backend_node_id: int,
69
+ session_id: str | None = None,
70
+ ):
71
+ self._browser_session = browser_session
72
+ self._client = browser_session.cdp_client
73
+ self._backend_node_id = backend_node_id
74
+ self._session_id = session_id
75
+
76
+ async def _get_node_id(self) -> int:
77
+ """Get DOM node ID from backend node ID."""
78
+ params: 'PushNodesByBackendIdsToFrontendParameters' = {'backendNodeIds': [self._backend_node_id]}
79
+ result = await self._client.send.DOM.pushNodesByBackendIdsToFrontend(params, session_id=self._session_id)
80
+ return result['nodeIds'][0]
81
+
82
+ async def _get_remote_object_id(self) -> str | None:
83
+ """Get remote object ID for this element."""
84
+ node_id = await self._get_node_id()
85
+ params: 'ResolveNodeParameters' = {'nodeId': node_id}
86
+ result = await self._client.send.DOM.resolveNode(params, session_id=self._session_id)
87
+ object_id = result['object'].get('objectId', None)
88
+
89
+ if not object_id:
90
+ return None
91
+ return object_id
92
+
93
+ async def click(
94
+ self,
95
+ button: 'MouseButton' = 'left',
96
+ click_count: int = 1,
97
+ modifiers: list[ModifierType] | None = None,
98
+ ) -> None:
99
+ """Click the element using the advanced watchdog implementation."""
100
+
101
+ try:
102
+ # Get viewport dimensions for visibility checks
103
+ layout_metrics = await self._client.send.Page.getLayoutMetrics(session_id=self._session_id)
104
+ viewport_width = layout_metrics['layoutViewport']['clientWidth']
105
+ viewport_height = layout_metrics['layoutViewport']['clientHeight']
106
+
107
+ # Try multiple methods to get element geometry
108
+ quads = []
109
+
110
+ # Method 1: Try DOM.getContentQuads first (best for inline elements and complex layouts)
111
+ try:
112
+ content_quads_result = await self._client.send.DOM.getContentQuads(
113
+ params={'backendNodeId': self._backend_node_id}, session_id=self._session_id
114
+ )
115
+ if 'quads' in content_quads_result and content_quads_result['quads']:
116
+ quads = content_quads_result['quads']
117
+ except Exception:
118
+ pass
119
+
120
+ # Method 2: Fall back to DOM.getBoxModel
121
+ if not quads:
122
+ try:
123
+ box_model = await self._client.send.DOM.getBoxModel(
124
+ params={'backendNodeId': self._backend_node_id}, session_id=self._session_id
125
+ )
126
+ if 'model' in box_model and 'content' in box_model['model']:
127
+ content_quad = box_model['model']['content']
128
+ if len(content_quad) >= 8:
129
+ # Convert box model format to quad format
130
+ quads = [
131
+ [
132
+ content_quad[0],
133
+ content_quad[1], # x1, y1
134
+ content_quad[2],
135
+ content_quad[3], # x2, y2
136
+ content_quad[4],
137
+ content_quad[5], # x3, y3
138
+ content_quad[6],
139
+ content_quad[7], # x4, y4
140
+ ]
141
+ ]
142
+ except Exception:
143
+ pass
144
+
145
+ # Method 3: Fall back to JavaScript getBoundingClientRect
146
+ if not quads:
147
+ try:
148
+ result = await self._client.send.DOM.resolveNode(
149
+ params={'backendNodeId': self._backend_node_id}, session_id=self._session_id
150
+ )
151
+ if 'object' in result and 'objectId' in result['object']:
152
+ object_id = result['object']['objectId']
153
+
154
+ # Get bounding rect via JavaScript
155
+ bounds_result = await self._client.send.Runtime.callFunctionOn(
156
+ params={
157
+ 'functionDeclaration': """
158
+ function() {
159
+ const rect = this.getBoundingClientRect();
160
+ return {
161
+ x: rect.left,
162
+ y: rect.top,
163
+ width: rect.width,
164
+ height: rect.height
165
+ };
166
+ }
167
+ """,
168
+ 'objectId': object_id,
169
+ 'returnByValue': True,
170
+ },
171
+ session_id=self._session_id,
172
+ )
173
+
174
+ if 'result' in bounds_result and 'value' in bounds_result['result']:
175
+ rect = bounds_result['result']['value']
176
+ # Convert rect to quad format
177
+ x, y, w, h = rect['x'], rect['y'], rect['width'], rect['height']
178
+ quads = [
179
+ [
180
+ x,
181
+ y, # top-left
182
+ x + w,
183
+ y, # top-right
184
+ x + w,
185
+ y + h, # bottom-right
186
+ x,
187
+ y + h, # bottom-left
188
+ ]
189
+ ]
190
+ except Exception:
191
+ pass
192
+
193
+ # If we still don't have quads, fall back to JS click
194
+ if not quads:
195
+ try:
196
+ result = await self._client.send.DOM.resolveNode(
197
+ params={'backendNodeId': self._backend_node_id}, session_id=self._session_id
198
+ )
199
+ if 'object' not in result or 'objectId' not in result['object']:
200
+ raise Exception('Failed to find DOM element based on backendNodeId, maybe page content changed?')
201
+ object_id = result['object']['objectId']
202
+
203
+ await self._client.send.Runtime.callFunctionOn(
204
+ params={
205
+ 'functionDeclaration': 'function() { this.click(); }',
206
+ 'objectId': object_id,
207
+ },
208
+ session_id=self._session_id,
209
+ )
210
+ await asyncio.sleep(0.05)
211
+ return
212
+ except Exception as js_e:
213
+ raise Exception(f'Failed to click element: {js_e}')
214
+
215
+ # Find the largest visible quad within the viewport
216
+ best_quad = None
217
+ best_area = 0
218
+
219
+ for quad in quads:
220
+ if len(quad) < 8:
221
+ continue
222
+
223
+ # Calculate quad bounds
224
+ xs = [quad[i] for i in range(0, 8, 2)]
225
+ ys = [quad[i] for i in range(1, 8, 2)]
226
+ min_x, max_x = min(xs), max(xs)
227
+ min_y, max_y = min(ys), max(ys)
228
+
229
+ # Check if quad intersects with viewport
230
+ if max_x < 0 or max_y < 0 or min_x > viewport_width or min_y > viewport_height:
231
+ continue # Quad is completely outside viewport
232
+
233
+ # Calculate visible area (intersection with viewport)
234
+ visible_min_x = max(0, min_x)
235
+ visible_max_x = min(viewport_width, max_x)
236
+ visible_min_y = max(0, min_y)
237
+ visible_max_y = min(viewport_height, max_y)
238
+
239
+ visible_width = visible_max_x - visible_min_x
240
+ visible_height = visible_max_y - visible_min_y
241
+ visible_area = visible_width * visible_height
242
+
243
+ if visible_area > best_area:
244
+ best_area = visible_area
245
+ best_quad = quad
246
+
247
+ if not best_quad:
248
+ # No visible quad found, use the first quad anyway
249
+ best_quad = quads[0]
250
+
251
+ # Calculate center point of the best quad
252
+ center_x = sum(best_quad[i] for i in range(0, 8, 2)) / 4
253
+ center_y = sum(best_quad[i] for i in range(1, 8, 2)) / 4
254
+
255
+ # Ensure click point is within viewport bounds
256
+ center_x = max(0, min(viewport_width - 1, center_x))
257
+ center_y = max(0, min(viewport_height - 1, center_y))
258
+
259
+ # Scroll element into view
260
+ try:
261
+ await self._client.send.DOM.scrollIntoViewIfNeeded(
262
+ params={'backendNodeId': self._backend_node_id}, session_id=self._session_id
263
+ )
264
+ await asyncio.sleep(0.05) # Wait for scroll to complete
265
+ except Exception:
266
+ pass
267
+
268
+ # Calculate modifier bitmask for CDP
269
+ modifier_value = 0
270
+ if modifiers:
271
+ modifier_map = {'Alt': 1, 'Control': 2, 'Meta': 4, 'Shift': 8}
272
+ for mod in modifiers:
273
+ modifier_value |= modifier_map.get(mod, 0)
274
+
275
+ # Perform the click using CDP
276
+ try:
277
+ # Move mouse to element
278
+ await self._client.send.Input.dispatchMouseEvent(
279
+ params={
280
+ 'type': 'mouseMoved',
281
+ 'x': center_x,
282
+ 'y': center_y,
283
+ },
284
+ session_id=self._session_id,
285
+ )
286
+ await asyncio.sleep(0.05)
287
+
288
+ # Mouse down
289
+ try:
290
+ await asyncio.wait_for(
291
+ self._client.send.Input.dispatchMouseEvent(
292
+ params={
293
+ 'type': 'mousePressed',
294
+ 'x': center_x,
295
+ 'y': center_y,
296
+ 'button': button,
297
+ 'clickCount': click_count,
298
+ 'modifiers': modifier_value,
299
+ },
300
+ session_id=self._session_id,
301
+ ),
302
+ timeout=1.0, # 1 second timeout for mousePressed
303
+ )
304
+ await asyncio.sleep(0.08)
305
+ except TimeoutError:
306
+ pass # Don't sleep if we timed out
307
+
308
+ # Mouse up
309
+ try:
310
+ await asyncio.wait_for(
311
+ self._client.send.Input.dispatchMouseEvent(
312
+ params={
313
+ 'type': 'mouseReleased',
314
+ 'x': center_x,
315
+ 'y': center_y,
316
+ 'button': button,
317
+ 'clickCount': click_count,
318
+ 'modifiers': modifier_value,
319
+ },
320
+ session_id=self._session_id,
321
+ ),
322
+ timeout=3.0, # 3 second timeout for mouseReleased
323
+ )
324
+ except TimeoutError:
325
+ pass
326
+
327
+ except Exception as e:
328
+ # Fall back to JavaScript click via CDP
329
+ try:
330
+ result = await self._client.send.DOM.resolveNode(
331
+ params={'backendNodeId': self._backend_node_id}, session_id=self._session_id
332
+ )
333
+ if 'object' not in result or 'objectId' not in result['object']:
334
+ raise Exception('Failed to find DOM element based on backendNodeId, maybe page content changed?')
335
+ object_id = result['object']['objectId']
336
+
337
+ await self._client.send.Runtime.callFunctionOn(
338
+ params={
339
+ 'functionDeclaration': 'function() { this.click(); }',
340
+ 'objectId': object_id,
341
+ },
342
+ session_id=self._session_id,
343
+ )
344
+ await asyncio.sleep(0.1)
345
+ return
346
+ except Exception as js_e:
347
+ raise Exception(f'Failed to click element: {e}')
348
+
349
+ except Exception as e:
350
+ # Extract key element info for error message
351
+ raise RuntimeError(f'Failed to click element: {e}')
352
+
353
+ async def fill(self, value: str, clear: bool = True) -> None:
354
+ """Fill the input element using proper CDP methods with improved focus handling."""
355
+ try:
356
+ # Use the existing CDP client and session
357
+ cdp_client = self._client
358
+ session_id = self._session_id
359
+ backend_node_id = self._backend_node_id
360
+
361
+ # Track coordinates for metadata
362
+ input_coordinates = None
363
+
364
+ # Scroll element into view
365
+ try:
366
+ await cdp_client.send.DOM.scrollIntoViewIfNeeded(params={'backendNodeId': backend_node_id}, session_id=session_id)
367
+ await asyncio.sleep(0.01)
368
+ except Exception as e:
369
+ logger.warning(f'Failed to scroll element into view: {e}')
370
+
371
+ # Get object ID for the element
372
+ result = await cdp_client.send.DOM.resolveNode(
373
+ params={'backendNodeId': backend_node_id},
374
+ session_id=session_id,
375
+ )
376
+ if 'object' not in result or 'objectId' not in result['object']:
377
+ raise RuntimeError('Failed to get object ID for element')
378
+ object_id = result['object']['objectId']
379
+
380
+ # Get element coordinates for focus
381
+ try:
382
+ bounds_result = await cdp_client.send.Runtime.callFunctionOn(
383
+ params={
384
+ 'functionDeclaration': 'function() { return this.getBoundingClientRect(); }',
385
+ 'objectId': object_id,
386
+ 'returnByValue': True,
387
+ },
388
+ session_id=session_id,
389
+ )
390
+ if bounds_result.get('result', {}).get('value'):
391
+ bounds = bounds_result['result']['value'] # type: ignore
392
+ center_x = bounds['x'] + bounds['width'] / 2
393
+ center_y = bounds['y'] + bounds['height'] / 2
394
+ input_coordinates = {'input_x': center_x, 'input_y': center_y}
395
+ logger.debug(f'Using element coordinates: x={center_x:.1f}, y={center_y:.1f}')
396
+ except Exception as e:
397
+ logger.debug(f'Could not get element coordinates: {e}')
398
+
399
+ # Ensure session_id is not None
400
+ if session_id is None:
401
+ raise RuntimeError('Session ID is required for fill operation')
402
+
403
+ # Step 1: Focus the element
404
+ focused_successfully = await self._focus_element_simple(
405
+ backend_node_id=backend_node_id,
406
+ object_id=object_id,
407
+ cdp_client=cdp_client,
408
+ session_id=session_id,
409
+ input_coordinates=input_coordinates,
410
+ )
411
+
412
+ # Step 2: Clear existing text if requested
413
+ if clear:
414
+ cleared_successfully = await self._clear_text_field(
415
+ object_id=object_id, cdp_client=cdp_client, session_id=session_id
416
+ )
417
+ if not cleared_successfully:
418
+ logger.warning('Text field clearing failed, typing may append to existing text')
419
+
420
+ # Step 3: Type the text character by character using proper human-like key events
421
+ logger.debug(f'Typing text character by character: "{value}"')
422
+
423
+ for i, char in enumerate(value):
424
+ # Handle newline characters as Enter key
425
+ if char == '\n':
426
+ # Send proper Enter key sequence
427
+ await cdp_client.send.Input.dispatchKeyEvent(
428
+ params={
429
+ 'type': 'keyDown',
430
+ 'key': 'Enter',
431
+ 'code': 'Enter',
432
+ 'windowsVirtualKeyCode': 13,
433
+ },
434
+ session_id=session_id,
435
+ )
436
+
437
+ # Small delay to emulate human typing speed
438
+ await asyncio.sleep(0.001)
439
+
440
+ # Send char event with carriage return
441
+ await cdp_client.send.Input.dispatchKeyEvent(
442
+ params={
443
+ 'type': 'char',
444
+ 'text': '\r',
445
+ 'key': 'Enter',
446
+ },
447
+ session_id=session_id,
448
+ )
449
+
450
+ # Send keyUp event
451
+ await cdp_client.send.Input.dispatchKeyEvent(
452
+ params={
453
+ 'type': 'keyUp',
454
+ 'key': 'Enter',
455
+ 'code': 'Enter',
456
+ 'windowsVirtualKeyCode': 13,
457
+ },
458
+ session_id=session_id,
459
+ )
460
+ else:
461
+ # Handle regular characters
462
+ # Get proper modifiers, VK code, and base key for the character
463
+ modifiers, vk_code, base_key = self._get_char_modifiers_and_vk(char)
464
+ key_code = self._get_key_code_for_char(base_key)
465
+
466
+ # Step 1: Send keyDown event (NO text parameter)
467
+ await cdp_client.send.Input.dispatchKeyEvent(
468
+ params={
469
+ 'type': 'keyDown',
470
+ 'key': base_key,
471
+ 'code': key_code,
472
+ 'modifiers': modifiers,
473
+ 'windowsVirtualKeyCode': vk_code,
474
+ },
475
+ session_id=session_id,
476
+ )
477
+
478
+ # Small delay to emulate human typing speed
479
+ await asyncio.sleep(0.001)
480
+
481
+ # Step 2: Send char event (WITH text parameter) - this is crucial for text input
482
+ await cdp_client.send.Input.dispatchKeyEvent(
483
+ params={
484
+ 'type': 'char',
485
+ 'text': char,
486
+ 'key': char,
487
+ },
488
+ session_id=session_id,
489
+ )
490
+
491
+ # Step 3: Send keyUp event (NO text parameter)
492
+ await cdp_client.send.Input.dispatchKeyEvent(
493
+ params={
494
+ 'type': 'keyUp',
495
+ 'key': base_key,
496
+ 'code': key_code,
497
+ 'modifiers': modifiers,
498
+ 'windowsVirtualKeyCode': vk_code,
499
+ },
500
+ session_id=session_id,
501
+ )
502
+
503
+ # Add 18ms delay between keystrokes
504
+ await asyncio.sleep(0.018)
505
+
506
+ except Exception as e:
507
+ raise Exception(f'Failed to fill element: {str(e)}')
508
+
509
+ async def hover(self) -> None:
510
+ """Hover over the element."""
511
+ box = await self.get_bounding_box()
512
+ if not box:
513
+ raise RuntimeError('Element is not visible or has no bounding box')
514
+
515
+ x = box['x'] + box['width'] / 2
516
+ y = box['y'] + box['height'] / 2
517
+
518
+ params: 'DispatchMouseEventParameters' = {'type': 'mouseMoved', 'x': x, 'y': y}
519
+ await self._client.send.Input.dispatchMouseEvent(params, session_id=self._session_id)
520
+
521
+ async def focus(self) -> None:
522
+ """Focus the element."""
523
+ node_id = await self._get_node_id()
524
+ params: 'FocusParameters' = {'nodeId': node_id}
525
+ await self._client.send.DOM.focus(params, session_id=self._session_id)
526
+
527
+ async def check(self) -> None:
528
+ """Check or uncheck a checkbox/radio button."""
529
+ await self.click()
530
+
531
+ async def select_option(self, values: str | list[str]) -> None:
532
+ """Select option(s) in a select element."""
533
+ if isinstance(values, str):
534
+ values = [values]
535
+
536
+ # Focus the element first
537
+ try:
538
+ await self.focus()
539
+ except Exception:
540
+ logger.warning('Failed to focus element')
541
+
542
+ # For select elements, we need to find option elements and click them
543
+ # This is a simplified approach - in practice, you might need to handle
544
+ # different select types (single vs multi-select) differently
545
+ node_id = await self._get_node_id()
546
+
547
+ # Request child nodes to get the options
548
+ params: 'RequestChildNodesParameters' = {'nodeId': node_id, 'depth': 1}
549
+ await self._client.send.DOM.requestChildNodes(params, session_id=self._session_id)
550
+
551
+ # Get the updated node description with children
552
+ describe_params: 'DescribeNodeParameters' = {'nodeId': node_id, 'depth': 1}
553
+ describe_result = await self._client.send.DOM.describeNode(describe_params, session_id=self._session_id)
554
+
555
+ select_node = describe_result['node']
556
+
557
+ # Find and select matching options
558
+ for child in select_node.get('children', []):
559
+ if child.get('nodeName', '').lower() == 'option':
560
+ # Get option attributes
561
+ attrs = child.get('attributes', [])
562
+ option_attrs = {}
563
+ for i in range(0, len(attrs), 2):
564
+ if i + 1 < len(attrs):
565
+ option_attrs[attrs[i]] = attrs[i + 1]
566
+
567
+ option_value = option_attrs.get('value', '')
568
+ option_text = child.get('nodeValue', '')
569
+
570
+ # Check if this option should be selected
571
+ should_select = option_value in values or option_text in values
572
+
573
+ if should_select:
574
+ # Click the option to select it
575
+ option_node_id = child.get('nodeId')
576
+ if option_node_id:
577
+ # Get backend node ID for the option
578
+ option_describe_params: 'DescribeNodeParameters' = {'nodeId': option_node_id}
579
+ option_backend_result = await self._client.send.DOM.describeNode(
580
+ option_describe_params, session_id=self._session_id
581
+ )
582
+ option_backend_id = option_backend_result['node']['backendNodeId']
583
+
584
+ # Create an Element for the option and click it
585
+ option_element = Element(self._browser_session, option_backend_id, self._session_id)
586
+ await option_element.click()
587
+
588
+ async def drag_to(
589
+ self,
590
+ target: Union['Element', Position],
591
+ source_position: Position | None = None,
592
+ target_position: Position | None = None,
593
+ ) -> None:
594
+ """Drag this element to another element or position."""
595
+ # Get source coordinates
596
+ if source_position:
597
+ source_x = source_position['x']
598
+ source_y = source_position['y']
599
+ else:
600
+ source_box = await self.get_bounding_box()
601
+ if not source_box:
602
+ raise RuntimeError('Source element is not visible')
603
+ source_x = source_box['x'] + source_box['width'] / 2
604
+ source_y = source_box['y'] + source_box['height'] / 2
605
+
606
+ # Get target coordinates
607
+ if isinstance(target, dict) and 'x' in target and 'y' in target:
608
+ target_x = target['x']
609
+ target_y = target['y']
610
+ else:
611
+ if target_position:
612
+ target_box = await target.get_bounding_box()
613
+ if not target_box:
614
+ raise RuntimeError('Target element is not visible')
615
+ target_x = target_box['x'] + target_position['x']
616
+ target_y = target_box['y'] + target_position['y']
617
+ else:
618
+ target_box = await target.get_bounding_box()
619
+ if not target_box:
620
+ raise RuntimeError('Target element is not visible')
621
+ target_x = target_box['x'] + target_box['width'] / 2
622
+ target_y = target_box['y'] + target_box['height'] / 2
623
+
624
+ # Perform drag operation
625
+ await self._client.send.Input.dispatchMouseEvent(
626
+ {'type': 'mousePressed', 'x': source_x, 'y': source_y, 'button': 'left'},
627
+ session_id=self._session_id,
628
+ )
629
+
630
+ await self._client.send.Input.dispatchMouseEvent(
631
+ {'type': 'mouseMoved', 'x': target_x, 'y': target_y},
632
+ session_id=self._session_id,
633
+ )
634
+
635
+ await self._client.send.Input.dispatchMouseEvent(
636
+ {'type': 'mouseReleased', 'x': target_x, 'y': target_y, 'button': 'left'},
637
+ session_id=self._session_id,
638
+ )
639
+
640
+ # Element properties and queries
641
+ async def get_attribute(self, name: str) -> str | None:
642
+ """Get an attribute value."""
643
+ node_id = await self._get_node_id()
644
+ params: 'GetAttributesParameters' = {'nodeId': node_id}
645
+ result = await self._client.send.DOM.getAttributes(params, session_id=self._session_id)
646
+
647
+ attributes = result['attributes']
648
+ for i in range(0, len(attributes), 2):
649
+ if attributes[i] == name:
650
+ return attributes[i + 1]
651
+ return None
652
+
653
+ async def get_bounding_box(self) -> BoundingBox | None:
654
+ """Get the bounding box of the element."""
655
+ try:
656
+ node_id = await self._get_node_id()
657
+ params: 'GetBoxModelParameters' = {'nodeId': node_id}
658
+ result = await self._client.send.DOM.getBoxModel(params, session_id=self._session_id)
659
+
660
+ if 'model' not in result:
661
+ return None
662
+
663
+ # Get content box (first 8 values are content quad: x1,y1,x2,y2,x3,y3,x4,y4)
664
+ content = result['model']['content']
665
+ if len(content) < 8:
666
+ return None
667
+
668
+ # Calculate bounding box from quad
669
+ x_coords = [content[i] for i in range(0, 8, 2)]
670
+ y_coords = [content[i] for i in range(1, 8, 2)]
671
+
672
+ x = min(x_coords)
673
+ y = min(y_coords)
674
+ width = max(x_coords) - x
675
+ height = max(y_coords) - y
676
+
677
+ return BoundingBox(x=x, y=y, width=width, height=height)
678
+
679
+ except Exception:
680
+ return None
681
+
682
+ async def screenshot(self, format: str = 'jpeg', quality: int | None = None) -> str:
683
+ """Take a screenshot of this element and return base64 encoded image.
684
+
685
+ Args:
686
+ format: Image format ('jpeg', 'png', 'webp')
687
+ quality: Quality 0-100 for JPEG format
688
+
689
+ Returns:
690
+ Base64-encoded image data
691
+ """
692
+ # Get element's bounding box
693
+ box = await self.get_bounding_box()
694
+ if not box:
695
+ raise RuntimeError('Element is not visible or has no bounding box')
696
+
697
+ # Create viewport clip for the element
698
+ viewport: 'Viewport' = {'x': box['x'], 'y': box['y'], 'width': box['width'], 'height': box['height'], 'scale': 1.0}
699
+
700
+ # Prepare screenshot parameters
701
+ params: 'CaptureScreenshotParameters' = {'format': format, 'clip': viewport}
702
+
703
+ if quality is not None and format.lower() == 'jpeg':
704
+ params['quality'] = quality
705
+
706
+ # Take screenshot
707
+ result = await self._client.send.Page.captureScreenshot(params, session_id=self._session_id)
708
+
709
+ return result['data']
710
+
711
+ async def evaluate(self, page_function: str, *args) -> str:
712
+ """Execute JavaScript code in the context of this element.
713
+
714
+ The JavaScript code executes with 'this' bound to the element, allowing direct
715
+ access to element properties and methods.
716
+
717
+ Args:
718
+ page_function: JavaScript code that MUST start with (...args) => format
719
+ *args: Arguments to pass to the function
720
+
721
+ Returns:
722
+ String representation of the JavaScript execution result.
723
+ Objects and arrays are JSON-stringified.
724
+
725
+ Example:
726
+ # Get element's text content
727
+ text = await element.evaluate("() => this.textContent")
728
+
729
+ # Set style with argument
730
+ await element.evaluate("(color) => this.style.color = color", "red")
731
+
732
+ # Get computed style
733
+ color = await element.evaluate("() => getComputedStyle(this).color")
734
+
735
+ # Async operations
736
+ result = await element.evaluate("async () => { await new Promise(r => setTimeout(r, 100)); return this.id; }")
737
+ """
738
+ # Get remote object ID for this element
739
+ object_id = await self._get_remote_object_id()
740
+ if not object_id:
741
+ raise RuntimeError('Element has no remote object ID (element may be detached from DOM)')
742
+
743
+ # Validate arrow function format (allow async prefix)
744
+ page_function = page_function.strip()
745
+ # Check for arrow function with optional async prefix
746
+ if not ('=>' in page_function and (page_function.startswith('(') or page_function.startswith('async'))):
747
+ raise ValueError(
748
+ f'JavaScript code must start with (...args) => or async (...args) => format. Got: {page_function[:50]}...'
749
+ )
750
+
751
+ # Convert arrow function to function declaration for CallFunctionOn
752
+ # CallFunctionOn expects 'function(...args) { ... }' format, not arrow functions
753
+ # We need to convert: '() => expression' to 'function() { return expression; }'
754
+ # or: '(x, y) => { statements }' to 'function(x, y) { statements }'
755
+
756
+ # Extract parameters and body from arrow function
757
+ import re
758
+
759
+ # Check if it's an async arrow function
760
+ is_async = page_function.strip().startswith('async')
761
+ async_prefix = 'async ' if is_async else ''
762
+
763
+ # Match: (params) => body or async (params) => body
764
+ # Strip 'async' prefix if present for parsing
765
+ func_to_parse = page_function.strip()
766
+ if is_async:
767
+ func_to_parse = func_to_parse[5:].strip() # Remove 'async' prefix
768
+
769
+ arrow_match = re.match(r'\s*\(([^)]*)\)\s*=>\s*(.+)', func_to_parse, re.DOTALL)
770
+ if not arrow_match:
771
+ raise ValueError(f'Could not parse arrow function: {page_function[:50]}...')
772
+
773
+ params_str = arrow_match.group(1).strip() # e.g., '', 'x', 'x, y'
774
+ body = arrow_match.group(2).strip()
775
+
776
+ # If body doesn't start with {, it's an expression that needs implicit return
777
+ if not body.startswith('{'):
778
+ function_declaration = f'{async_prefix}function({params_str}) {{ return {body}; }}'
779
+ else:
780
+ # Body already has braces, use as-is
781
+ function_declaration = f'{async_prefix}function({params_str}) {body}'
782
+
783
+ # Build CallArgument list for args if provided
784
+ call_arguments = []
785
+ if args:
786
+ from cdp_use.cdp.runtime.types import CallArgument
787
+
788
+ for arg in args:
789
+ # Convert Python values to CallArgument format
790
+ call_arguments.append(CallArgument(value=arg))
791
+
792
+ # Prepare CallFunctionOn parameters
793
+
794
+ params: 'CallFunctionOnParameters' = {
795
+ 'functionDeclaration': function_declaration,
796
+ 'objectId': object_id,
797
+ 'returnByValue': True,
798
+ 'awaitPromise': True,
799
+ }
800
+
801
+ if call_arguments:
802
+ params['arguments'] = call_arguments
803
+
804
+ # Execute the function on the element
805
+ result = await self._client.send.Runtime.callFunctionOn(
806
+ params,
807
+ session_id=self._session_id,
808
+ )
809
+
810
+ # Handle exceptions
811
+ if 'exceptionDetails' in result:
812
+ raise RuntimeError(f'JavaScript evaluation failed: {result["exceptionDetails"]}')
813
+
814
+ # Extract and return value
815
+ value = result.get('result', {}).get('value')
816
+
817
+ # Return string representation (matching Page.evaluate behavior)
818
+ if value is None:
819
+ return ''
820
+ elif isinstance(value, str):
821
+ return value
822
+ else:
823
+ # Convert objects, numbers, booleans to string
824
+ import json
825
+
826
+ try:
827
+ return json.dumps(value) if isinstance(value, (dict, list)) else str(value)
828
+ except (TypeError, ValueError):
829
+ return str(value)
830
+
831
+ # Helpers for modifiers etc
832
+ def _get_char_modifiers_and_vk(self, char: str) -> tuple[int, int, str]:
833
+ """Get modifiers, virtual key code, and base key for a character.
834
+
835
+ Returns:
836
+ (modifiers, windowsVirtualKeyCode, base_key)
837
+ """
838
+ # Characters that require Shift modifier
839
+ shift_chars = {
840
+ '!': ('1', 49),
841
+ '@': ('2', 50),
842
+ '#': ('3', 51),
843
+ '$': ('4', 52),
844
+ '%': ('5', 53),
845
+ '^': ('6', 54),
846
+ '&': ('7', 55),
847
+ '*': ('8', 56),
848
+ '(': ('9', 57),
849
+ ')': ('0', 48),
850
+ '_': ('-', 189),
851
+ '+': ('=', 187),
852
+ '{': ('[', 219),
853
+ '}': (']', 221),
854
+ '|': ('\\', 220),
855
+ ':': (';', 186),
856
+ '"': ("'", 222),
857
+ '<': (',', 188),
858
+ '>': ('.', 190),
859
+ '?': ('/', 191),
860
+ '~': ('`', 192),
861
+ }
862
+
863
+ # Check if character requires Shift
864
+ if char in shift_chars:
865
+ base_key, vk_code = shift_chars[char]
866
+ return (8, vk_code, base_key) # Shift=8
867
+
868
+ # Uppercase letters require Shift
869
+ if char.isupper():
870
+ return (8, ord(char), char.lower()) # Shift=8
871
+
872
+ # Lowercase letters
873
+ if char.islower():
874
+ return (0, ord(char.upper()), char)
875
+
876
+ # Numbers
877
+ if char.isdigit():
878
+ return (0, ord(char), char)
879
+
880
+ # Special characters without Shift
881
+ no_shift_chars = {
882
+ ' ': 32,
883
+ '-': 189,
884
+ '=': 187,
885
+ '[': 219,
886
+ ']': 221,
887
+ '\\': 220,
888
+ ';': 186,
889
+ "'": 222,
890
+ ',': 188,
891
+ '.': 190,
892
+ '/': 191,
893
+ '`': 192,
894
+ }
895
+
896
+ if char in no_shift_chars:
897
+ return (0, no_shift_chars[char], char)
898
+
899
+ # Fallback
900
+ return (0, ord(char.upper()) if char.isalpha() else ord(char), char)
901
+
902
+ def _get_key_code_for_char(self, char: str) -> str:
903
+ """Get the proper key code for a character (like Playwright does)."""
904
+ # Key code mapping for common characters (using proper base keys + modifiers)
905
+ key_codes = {
906
+ ' ': 'Space',
907
+ '.': 'Period',
908
+ ',': 'Comma',
909
+ '-': 'Minus',
910
+ '_': 'Minus', # Underscore uses Minus with Shift
911
+ '@': 'Digit2', # @ uses Digit2 with Shift
912
+ '!': 'Digit1', # ! uses Digit1 with Shift (not 'Exclamation')
913
+ '?': 'Slash', # ? uses Slash with Shift
914
+ ':': 'Semicolon', # : uses Semicolon with Shift
915
+ ';': 'Semicolon',
916
+ '(': 'Digit9', # ( uses Digit9 with Shift
917
+ ')': 'Digit0', # ) uses Digit0 with Shift
918
+ '[': 'BracketLeft',
919
+ ']': 'BracketRight',
920
+ '{': 'BracketLeft', # { uses BracketLeft with Shift
921
+ '}': 'BracketRight', # } uses BracketRight with Shift
922
+ '/': 'Slash',
923
+ '\\': 'Backslash',
924
+ '=': 'Equal',
925
+ '+': 'Equal', # + uses Equal with Shift
926
+ '*': 'Digit8', # * uses Digit8 with Shift
927
+ '&': 'Digit7', # & uses Digit7 with Shift
928
+ '%': 'Digit5', # % uses Digit5 with Shift
929
+ '$': 'Digit4', # $ uses Digit4 with Shift
930
+ '#': 'Digit3', # # uses Digit3 with Shift
931
+ '^': 'Digit6', # ^ uses Digit6 with Shift
932
+ '~': 'Backquote', # ~ uses Backquote with Shift
933
+ '`': 'Backquote',
934
+ '"': 'Quote', # " uses Quote with Shift
935
+ "'": 'Quote',
936
+ '<': 'Comma', # < uses Comma with Shift
937
+ '>': 'Period', # > uses Period with Shift
938
+ '|': 'Backslash', # | uses Backslash with Shift
939
+ }
940
+
941
+ if char in key_codes:
942
+ return key_codes[char]
943
+ elif char.isalpha():
944
+ return f'Key{char.upper()}'
945
+ elif char.isdigit():
946
+ return f'Digit{char}'
947
+ else:
948
+ # Fallback for unknown characters
949
+ return f'Key{char.upper()}' if char.isascii() and char.isalpha() else 'Unidentified'
950
+
951
+ async def _clear_text_field(self, object_id: str, cdp_client, session_id: str) -> bool:
952
+ """Clear text field using multiple strategies, starting with the most reliable."""
953
+ try:
954
+ # Strategy 1: Direct JavaScript value setting (most reliable for modern web apps)
955
+ logger.debug('Clearing text field using JavaScript value setting')
956
+
957
+ await cdp_client.send.Runtime.callFunctionOn(
958
+ params={
959
+ 'functionDeclaration': """
960
+ function() {
961
+ // Try to select all text first (only works on text-like inputs)
962
+ // This handles cases where cursor is in the middle of text
963
+ try {
964
+ this.select();
965
+ } catch (e) {
966
+ // Some input types (date, color, number, etc.) don't support select()
967
+ // That's fine, we'll just clear the value directly
968
+ }
969
+ // Set value to empty
970
+ this.value = "";
971
+ // Dispatch events to notify frameworks like React
972
+ this.dispatchEvent(new Event("input", { bubbles: true }));
973
+ this.dispatchEvent(new Event("change", { bubbles: true }));
974
+ return this.value;
975
+ }
976
+ """,
977
+ 'objectId': object_id,
978
+ 'returnByValue': True,
979
+ },
980
+ session_id=session_id,
981
+ )
982
+
983
+ # Verify clearing worked by checking the value
984
+ verify_result = await cdp_client.send.Runtime.callFunctionOn(
985
+ params={
986
+ 'functionDeclaration': 'function() { return this.value; }',
987
+ 'objectId': object_id,
988
+ 'returnByValue': True,
989
+ },
990
+ session_id=session_id,
991
+ )
992
+
993
+ current_value = verify_result.get('result', {}).get('value', '')
994
+ if not current_value:
995
+ logger.debug('Text field cleared successfully using JavaScript')
996
+ return True
997
+ else:
998
+ logger.debug(f'JavaScript clear partially failed, field still contains: "{current_value}"')
999
+
1000
+ except Exception as e:
1001
+ logger.debug(f'JavaScript clear failed: {e}')
1002
+
1003
+ # Strategy 2: Triple-click + Delete (fallback for stubborn fields)
1004
+ try:
1005
+ logger.debug('Fallback: Clearing using triple-click + Delete')
1006
+
1007
+ # Get element center coordinates for triple-click
1008
+ bounds_result = await cdp_client.send.Runtime.callFunctionOn(
1009
+ params={
1010
+ 'functionDeclaration': 'function() { return this.getBoundingClientRect(); }',
1011
+ 'objectId': object_id,
1012
+ 'returnByValue': True,
1013
+ },
1014
+ session_id=session_id,
1015
+ )
1016
+
1017
+ if bounds_result.get('result', {}).get('value'):
1018
+ bounds = bounds_result['result']['value'] # type: ignore # type: ignore
1019
+ center_x = bounds['x'] + bounds['width'] / 2
1020
+ center_y = bounds['y'] + bounds['height'] / 2
1021
+
1022
+ # Triple-click to select all text
1023
+ await cdp_client.send.Input.dispatchMouseEvent(
1024
+ params={
1025
+ 'type': 'mousePressed',
1026
+ 'x': center_x,
1027
+ 'y': center_y,
1028
+ 'button': 'left',
1029
+ 'clickCount': 3,
1030
+ },
1031
+ session_id=session_id,
1032
+ )
1033
+ await cdp_client.send.Input.dispatchMouseEvent(
1034
+ params={
1035
+ 'type': 'mouseReleased',
1036
+ 'x': center_x,
1037
+ 'y': center_y,
1038
+ 'button': 'left',
1039
+ 'clickCount': 3,
1040
+ },
1041
+ session_id=session_id,
1042
+ )
1043
+
1044
+ # Delete selected text
1045
+ await cdp_client.send.Input.dispatchKeyEvent(
1046
+ params={
1047
+ 'type': 'keyDown',
1048
+ 'key': 'Delete',
1049
+ 'code': 'Delete',
1050
+ },
1051
+ session_id=session_id,
1052
+ )
1053
+ await cdp_client.send.Input.dispatchKeyEvent(
1054
+ params={
1055
+ 'type': 'keyUp',
1056
+ 'key': 'Delete',
1057
+ 'code': 'Delete',
1058
+ },
1059
+ session_id=session_id,
1060
+ )
1061
+
1062
+ logger.debug('Text field cleared using triple-click + Delete')
1063
+ return True
1064
+
1065
+ except Exception as e:
1066
+ logger.debug(f'Triple-click clear failed: {e}')
1067
+
1068
+ # If all strategies failed
1069
+ logger.warning('All text clearing strategies failed')
1070
+ return False
1071
+
1072
+ async def _focus_element_simple(
1073
+ self, backend_node_id: int, object_id: str, cdp_client, session_id: str, input_coordinates=None
1074
+ ) -> bool:
1075
+ """Focus element using multiple strategies with robust fallbacks."""
1076
+ try:
1077
+ # Strategy 1: CDP focus (most reliable)
1078
+ logger.debug('Focusing element using CDP focus')
1079
+ await cdp_client.send.DOM.focus(params={'backendNodeId': backend_node_id}, session_id=session_id)
1080
+ logger.debug('Element focused successfully using CDP focus')
1081
+ return True
1082
+ except Exception as e:
1083
+ logger.debug(f'CDP focus failed: {e}, trying JavaScript focus')
1084
+
1085
+ try:
1086
+ # Strategy 2: JavaScript focus (fallback)
1087
+ logger.debug('Focusing element using JavaScript focus')
1088
+ await cdp_client.send.Runtime.callFunctionOn(
1089
+ params={
1090
+ 'functionDeclaration': 'function() { this.focus(); }',
1091
+ 'objectId': object_id,
1092
+ },
1093
+ session_id=session_id,
1094
+ )
1095
+ logger.debug('Element focused successfully using JavaScript')
1096
+ return True
1097
+ except Exception as e:
1098
+ logger.debug(f'JavaScript focus failed: {e}, trying click focus')
1099
+
1100
+ try:
1101
+ # Strategy 3: Click to focus (last resort)
1102
+ if input_coordinates:
1103
+ logger.debug(f'Focusing element by clicking at coordinates: {input_coordinates}')
1104
+ center_x = input_coordinates['input_x']
1105
+ center_y = input_coordinates['input_y']
1106
+
1107
+ # Click on the element to focus it
1108
+ await cdp_client.send.Input.dispatchMouseEvent(
1109
+ params={
1110
+ 'type': 'mousePressed',
1111
+ 'x': center_x,
1112
+ 'y': center_y,
1113
+ 'button': 'left',
1114
+ 'clickCount': 1,
1115
+ },
1116
+ session_id=session_id,
1117
+ )
1118
+ await cdp_client.send.Input.dispatchMouseEvent(
1119
+ params={
1120
+ 'type': 'mouseReleased',
1121
+ 'x': center_x,
1122
+ 'y': center_y,
1123
+ 'button': 'left',
1124
+ 'clickCount': 1,
1125
+ },
1126
+ session_id=session_id,
1127
+ )
1128
+ logger.debug('Element focused using click')
1129
+ return True
1130
+ else:
1131
+ logger.debug('No coordinates available for click focus')
1132
+ except Exception as e:
1133
+ logger.warning(f'All focus strategies failed: {e}')
1134
+ return False
1135
+
1136
+ async def get_basic_info(self) -> ElementInfo:
1137
+ """Get basic information about the element including coordinates and properties."""
1138
+ try:
1139
+ # Get basic node information
1140
+ node_id = await self._get_node_id()
1141
+ describe_result = await self._client.send.DOM.describeNode({'nodeId': node_id}, session_id=self._session_id)
1142
+
1143
+ node_info = describe_result['node']
1144
+
1145
+ # Get bounding box
1146
+ bounding_box = await self.get_bounding_box()
1147
+
1148
+ # Get attributes as a proper dict
1149
+ attributes_list = node_info.get('attributes', [])
1150
+ attributes_dict: dict[str, str] = {}
1151
+ for i in range(0, len(attributes_list), 2):
1152
+ if i + 1 < len(attributes_list):
1153
+ attributes_dict[attributes_list[i]] = attributes_list[i + 1]
1154
+
1155
+ return ElementInfo(
1156
+ backendNodeId=self._backend_node_id,
1157
+ nodeId=node_id,
1158
+ nodeName=node_info.get('nodeName', ''),
1159
+ nodeType=node_info.get('nodeType', 0),
1160
+ nodeValue=node_info.get('nodeValue'),
1161
+ attributes=attributes_dict,
1162
+ boundingBox=bounding_box,
1163
+ error=None,
1164
+ )
1165
+ except Exception as e:
1166
+ return ElementInfo(
1167
+ backendNodeId=self._backend_node_id,
1168
+ nodeId=None,
1169
+ nodeName='',
1170
+ nodeType=0,
1171
+ nodeValue=None,
1172
+ attributes={},
1173
+ boundingBox=None,
1174
+ error=str(e),
1175
+ )