Repository navigation
Expand file tree
/
Copy pathdom_utils.py
More file actions
576 lines (512 loc) Β· 30.6 KB
/
Copy pathdom_utils.py
File metadata and controls
576 lines (512 loc) Β· 30.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
# web_agent/dom_utils.py
import time
import random
import json
import os
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.action_chains import ActionChains
def human_type(element, text: str, delay: tuple = (0.05, 0.15)):
for char in text:
element.send_keys(char)
time.sleep(random.uniform(*delay))
def scroll_by(driver, px: int = 600, direction: str = "down"):
sign = -1 if direction == "up" else 1
script = f"""
window.scrollBy({{top: {sign * abs(px)}, behavior: 'smooth'}});
let scrollers = Array.from(document.querySelectorAll('*')).filter(el =>
el.scrollHeight > el.clientHeight &&
(window.getComputedStyle(el).overflowY === 'auto' || window.getComputedStyle(el).overflowY === 'scroll')
);
if(scrollers.length > 0) {{
let mainScroller = scrollers.sort((a,b) => (b.clientHeight * b.clientWidth) - (a.clientHeight * a.clientWidth))[0];
mainScroller.scrollBy({{top: {sign * abs(px)}, behavior: 'smooth'}});
}}
"""
try: driver.execute_script(script)
except Exception:
try:
body = driver.find_element(By.TAG_NAME, 'body')
body.send_keys(Keys.PAGE_DOWN if direction == 'down' else Keys.PAGE_UP)
except: pass
def try_selectors(driver, selectors, action_fn, timeout: int = 4) -> bool:
for sel in selectors:
try:
by_type = By.XPATH if sel.startswith("/") or sel.startswith("(") else By.CSS_SELECTOR
el = WebDriverWait(driver, timeout).until(EC.presence_of_element_located((by_type, sel)))
driver.execute_script("arguments[0].scrollIntoView({block:'center'});", el)
time.sleep(0.1) # Micro-pause for scroll to settle
if action_fn(el) is not False: return True
except Exception: continue
return False
def get_page_snapshot(driver) -> str:
script = """
let interactiveEls = document.querySelectorAll('a, button, input, textarea, select, [role="button"], [role="textbox"], [tabindex="0"], [contenteditable], img, h1, h2, [role="alert"], [role="status"], [role="dialog"]');
let elementsData = [];
let idCounter = 1;
let uploadedImageFound = false;
// Find active and visible dialog/modal
let activeDialog = Array.from(document.querySelectorAll('[role="dialog"], [role="alertdialog"], [aria-modal="true"], div.modal')).find(el => {
let r = el.getBoundingClientRect();
return r.width > 0 && r.height > 0 && window.getComputedStyle(el).display !== 'none' && window.getComputedStyle(el).visibility !== 'hidden';
});
interactiveEls.forEach(el => {
let rect = el.getBoundingClientRect();
if(rect.width > 0 && rect.height > 0 && rect.top >= 0 && rect.top <= window.innerHeight) {
let tag = el.tagName.toLowerCase();
let role = el.getAttribute('role');
if (tag === 'img' && rect.width > 100 && rect.height > 100 && !el.src.includes('icon')) uploadedImageFound = true;
if (tag === 'img') return;
let isInsidePopup = activeDialog && activeDialog.contains(el);
el.setAttribute('ai-id', idCounter);
let type = el.getAttribute('type') || '';
let isInput = (tag === 'input' || tag === 'textarea' || el.isContentEditable);
let isStatusText = (tag === 'h1' || tag === 'h2' || role === 'alert' || role === 'status' || role === 'dialog');
let hint = (el.placeholder || el.getAttribute('aria-label') || el.name || el.getAttribute('data-test-id') || el.id || '').trim();
let textStr = "";
if (isStatusText) {
let val = (el.innerText || '').trim();
if (val.length > 2) textStr = `[PAGE TEXT/STATUS]: "${val.substring(0,80)}"`;
} else if (isInput) {
let val = (el.value || el.innerText || '').trim();
if (type === 'file') {
if (val) textStr = `[FILE ALREADY UPLOADED]`;
else textStr = `[EMPTY FILE INPUT]`;
} else {
if (val && val !== hint) textStr = `[ALREADY FILLED: "${val.substring(0,25)}..."] - DO NOT USE THIS`;
else textStr = `[EMPTY]`;
}
textStr += ` (Hint: "${hint.substring(0,50)}")`;
} else {
let rawText = el.innerText || el.textContent || el.getAttribute('aria-label') || hint || '';
let btnText = rawText.trim().replace(/\\s+/g, ' ').substring(0, 60);
textStr = `${btnText}`;
}
if(textStr) {
if (activeDialog) {
if (isInsidePopup) {
elementsData.push(`[${idCounter}] <${tag} ${type}>: (INSIDE POPUP) ${textStr}`);
idCounter++;
}
// Background elements are completely ignored to prevent any distraction!
} else {
elementsData.push(`[${idCounter}] <${tag} ${type}>: ${textStr}`);
idCounter++;
}
} else {
idCounter++;
}
}
});
let alertMsg = uploadedImageFound ? "π¨ ALERT: Large image visible.\\n\\n" : "";
return alertMsg + elementsData.join('\\n');
"""
try:
visible_elements = driver.execute_script(script)
return f"URL: {driver.current_url}\nTITLE: {driver.title}\n\nVISIBLE ELEMENTS:\n{visible_elements}"
except Exception as e: return f"Error: {e}"
def execute_action(driver, action: dict) -> str:
raw_act = action.get("action", "")
if isinstance(raw_act, dict):
inner = raw_act
actual_action = inner.pop("type", inner.pop("action", ""))
action.update(inner)
action["action"] = actual_action
raw_act = actual_action
act = str(raw_act).lower().strip()
element_id = action.get("element_id")
log_details = {k:v for k,v in action.items() if k not in ('action', 'element_id', 'thought', 'memory_check', '_failed', 'wait_time')}
print(f" βΆ {act} | ID: {element_id or 'N/A'} | {json.dumps(log_details)}")
target_selectors = [f"[ai-id='{element_id}']"]
try:
if act in ["click", "type", "type_and_enter", "press_key", "hover"] and not element_id:
return "failed: missing element_id in JSON"
# π NAVIGATION & SYSTEM ACTIONS
if act == "wait":
return "ok" # Handled by dynamic wait block in main_agent
elif act == "refresh_page":
print(" π Refreshing the page...")
driver.refresh(); return "ok"
elif act == "go_back":
print(" π Going back to previous page...")
driver.back(); return "ok"
elif act == "go_to_url":
target_url = action.get("url", "")
if not target_url: return "failed: missing 'url' key in JSON"
print(f" π Navigating to: {target_url}")
driver.get(target_url)
# π AUTOMATIC COOKIE INJECTION ON MID-RUN NAVIGATION π
try:
from cookie_manager import inject_cookies
if "pinterest.com" in target_url:
inject_cookies(driver, "pinterest")
elif "facebook.com" in target_url:
inject_cookies(driver, "facebook")
elif "linkedin.com" in target_url:
inject_cookies(driver, "linkedin")
elif "amazon.in" in target_url:
inject_cookies(driver, "amazon")
elif "tumblr.com" in target_url:
inject_cookies(driver, "tumblr")
except Exception as cookie_err:
print(f" β οΈ Mid-run cookie injection skipped: {cookie_err}")
return "ok"
elif act == "scroll_down": scroll_by(driver, action.get("pixels", 600), "down")
elif act == "scroll_up": scroll_by(driver, action.get("pixels", 600), "up")
elif act == "step_done": return "step_done"
elif act == "task_done": return "task_done"
# π INTERACTION ACTIONS
elif act == "click" and element_id:
# π― 1. CDP VISUAL COORDINATES FAST-PATH
click_x, click_y = None, None
cdp_elements = getattr(driver, "current_cdp_elements", [])
if cdp_elements:
for cel in cdp_elements:
if str(cel.get("ai_id")) == str(element_id):
click_x = cel.get("click_x")
click_y = cel.get("click_y")
print(f" π― [CDP NATIVE CLICK] Target [{element_id}] '{cel.get('label')}' -> Coordinates: ({click_x}, {click_y})")
break
if click_x is not None and click_y is not None:
try:
driver.execute_cdp_cmd('Input.dispatchMouseEvent', {
'type': 'mousePressed',
'x': int(click_x),
'y': int(click_y),
'button': 'left',
'clickCount': 1
})
time.sleep(0.05)
driver.execute_cdp_cmd('Input.dispatchMouseEvent', {
'type': 'mouseReleased',
'x': int(click_x),
'y': int(click_y),
'button': 'left',
'clickCount': 1
})
return "ok"
except Exception as cdp_err:
print(f" β οΈ CDP native click failed: {cdp_err}. Falling back to DOM selector...")
# π― 2. DOM SELECTOR FALLBACK
def _do_click(el):
try: el.click()
except: driver.execute_script("arguments[0].click();", el)
return True
if not try_selectors(driver, target_selectors, _do_click): return "failed: element not clickable"
elif act == "hover" and element_id:
click_x, click_y = None, None
cdp_elements = getattr(driver, "current_cdp_elements", [])
if cdp_elements:
for cel in cdp_elements:
if str(cel.get("ai_id")) == str(element_id):
click_x = cel.get("click_x")
click_y = cel.get("click_y")
print(f" π― [CDP NATIVE HOVER] Target [{element_id}] '{cel.get('label')}' -> Coordinates: ({click_x}, {click_y})")
break
if click_x is not None and click_y is not None:
try:
driver.execute_cdp_cmd('Input.dispatchMouseEvent', {'type': 'mouseMoved', 'x': int(click_x), 'y': int(click_y)})
return "ok"
except Exception as cdp_h_err:
print(f" β οΈ CDP native hover failed: {cdp_h_err}. Falling back to DOM selector...")
def _do_hover(el):
try: driver.execute_script("arguments[0].scrollIntoView({block:'center'});", el); time.sleep(0.1)
except: pass
ActionChains(driver).move_to_element(el).perform()
return True
if not try_selectors(driver, target_selectors, _do_hover): return "failed: element not hoverable"
elif act in ["type", "type_and_enter"] and element_id:
text_to_type = action.get("text", "") or action.get("value", "") or action.get("content", "")
if not text_to_type: return "failed: missing 'text' key in JSON"
# Clean up any literal "\n" or "/n" strings to actual newlines
if isinstance(text_to_type, str):
text_to_type = text_to_type.replace("\\n", "\n").replace("/n", "\n")
# π― 1. CDP VISUAL COORDINATES FAST-PATH
click_x, click_y = None, None
cdp_elements = getattr(driver, "current_cdp_elements", [])
if cdp_elements:
for cel in cdp_elements:
if str(cel.get("ai_id")) == str(element_id):
click_x = cel.get("click_x")
click_y = cel.get("click_y")
print(f" π― [CDP NATIVE TYPE] Target [{element_id}] '{cel.get('label')}' -> Coordinates: ({click_x}, {click_y})")
break
if click_x is not None and click_y is not None:
try:
driver.execute_cdp_cmd('Input.dispatchMouseEvent', {'type': 'mousePressed', 'x': int(click_x), 'y': int(click_y), 'button': 'left', 'clickCount': 1})
time.sleep(0.05)
driver.execute_cdp_cmd('Input.dispatchMouseEvent', {'type': 'mouseReleased', 'x': int(click_x), 'y': int(click_y), 'button': 'left', 'clickCount': 1})
time.sleep(0.5)
actions = ActionChains(driver)
actions.send_keys(str(text_to_type))
if act == "type_and_enter":
actions.send_keys(Keys.ENTER)
actions.perform()
return "ok"
except Exception as cdp_type_err:
print(f" β οΈ CDP native typing failed: {cdp_type_err}. Falling back to DOM selector...")
# π― 2. DOM SELECTOR FALLBACK
def _do_type_fallback(el):
try: driver.execute_script("arguments[0].scrollIntoView({block:'center'}); arguments[0].focus();", el); time.sleep(0.5); el.click()
except: driver.execute_script("arguments[0].focus(); arguments[0].click();", el)
time.sleep(0.5)
is_contenteditable = False
try: is_contenteditable = driver.execute_script("return arguments[0].isContentEditable;", el)
except: pass
if not is_contenteditable:
try: el.clear()
except:
try: driver.execute_script("arguments[0].value = '';", el)
except: pass
try: ActionChains(driver).key_up(Keys.SHIFT).key_up(Keys.CONTROL).key_up(Keys.ALT).send_keys(Keys.NULL).perform()
except: pass
try: el.send_keys(Keys.NULL)
except: pass
try:
el.send_keys(str(text_to_type))
time.sleep(0.5)
actual = str(el.get_attribute("value") or el.text or "")
if len(actual.strip()) < len(str(text_to_type)) * 0.5:
raise Exception("React dropped keys or sticky keys issue")
if act == "type_and_enter":
el.send_keys(Keys.ENTER)
except Exception:
try: ActionChains(driver).key_up(Keys.SHIFT).key_up(Keys.CONTROL).key_up(Keys.ALT).send_keys(Keys.NULL).perform()
except: pass
try:
driver.execute_script("""
try {
let el = arguments[0];
let text = arguments[1];
el.focus();
if(el.isContentEditable) {
document.execCommand('insertText', false, text);
} else {
let nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value")?.set;
let nativeTextAreaValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, "value")?.set;
if (el.tagName.toLowerCase() === 'textarea' && nativeTextAreaValueSetter) {
nativeTextAreaValueSetter.call(el, text);
} else if (nativeInputValueSetter) {
nativeInputValueSetter.call(el, text);
} else {
el.value = text;
}
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
}
} catch(e) { console.error(e); }
""", el, str(text_to_type))
time.sleep(0.5)
if act == "type_and_enter":
try: ActionChains(driver).send_keys(Keys.ENTER).perform()
except: el.send_keys(Keys.ENTER)
except Exception as e:
print(f"JS Fallback error: {e}")
return True
if not try_selectors(driver, target_selectors, _do_type_fallback): return "failed: element not typable"
elif act == "upload_file":
file_path = action.get("path", "") or action.get("file_path", "") or action.get("text", "")
if not file_path: return "failed: missing 'file_path' key in JSON"
# π§Ή Smart clean file_path for Windows double escape/slash issues and trailing quotes
import re
file_path = file_path.strip("'\" \t\n\r")
file_path = re.sub(r'\\+', r'\\', file_path)
file_path = file_path.replace("//", "/")
file_path = os.path.abspath(file_path)
# π Immediate path existence check
if not os.path.exists(file_path):
# Fallback to test.png in the same directory if the provided file does not exist
fallback_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "test.png")
if os.path.exists(fallback_path):
print(f"β οΈ [UPLOAD WARNING] Path '{file_path}' not found on disk. Falling back to local test image: {fallback_path}")
file_path = fallback_path
else:
print(f"β [UPLOAD ERROR] Local file path does not exist on disk: {file_path}")
return f"failed: local file path does not exist: {file_path}"
print(f"π [UPLOAD] Verified local file exists on disk: {file_path}")
# Special clipboard paste flow for Medium.com on Windows
if "medium.com" in driver.current_url:
print(" π [MEDIUM UPLOAD] Special clipboard paste flow initiated...")
import subprocess
import platform
if platform.system() == 'Windows':
try:
print(f" π Copying image to clipboard: {file_path}")
res = subprocess.run(["powershell", "-Command", f"Set-Clipboard -Path '{file_path}'"], capture_output=True, text=True)
if res.returncode != 0:
print(f" β PowerShell clipboard copy failed: {res.stderr}")
else:
editor_el = None
try:
active_el = driver.switch_to.active_element
if active_el and active_el.tag_name.lower() != "body":
editor_el = active_el
except:
pass
if not editor_el:
try:
editor_el = driver.find_element(By.CSS_SELECTOR, '[contenteditable="true"]')
except:
pass
if not editor_el and element_id:
try:
editor_el = driver.find_element(By.CSS_SELECTOR, f"[ai-id='{element_id}']")
except:
pass
if editor_el:
print(f" π― Focusing editor element for paste: {editor_el.tag_name}...")
try:
driver.execute_script("arguments[0].scrollIntoView({block:'center'}); arguments[0].focus();", editor_el)
time.sleep(0.5)
editor_el.click()
except Exception as focus_err:
print(f" β οΈ Focus failed: {focus_err}")
time.sleep(1.0)
print(" β¨οΈ Sending Ctrl+V...")
actions = ActionChains(driver)
actions.key_down(Keys.CONTROL).send_keys('v').key_up(Keys.CONTROL).perform()
print(" β³ Waiting 8 seconds for image upload to process...")
time.sleep(8.0)
return "ok"
else:
print(" β οΈ Could not find contenteditable element for Medium upload.")
except Exception as paste_err:
print(f" β οΈ Medium clipboard paste flow exception: {paste_err}")
# 1. Standard DOM Injection
if element_id:
upload_success = [False]
last_exception = [None]
def _do_upload(el):
try:
resolved_input = None
# 1. Check if el itself is input[type='file']
if el.tag_name == "input" and el.get_attribute("type") == "file":
resolved_input = el
# 2. Check if there is an input[type='file'] inside el
if not resolved_input:
try:
resolved_input = el.find_element(By.CSS_SELECTOR, "input[type='file']")
print(f" π [NESTED INPUT FOUND] Found file input inside element {element_id}!")
except Exception:
pass
# 3. Check if there is an input[type='file'] inside the active dialog/popup
if not resolved_input:
try:
active_dialog = driver.find_element(By.CSS_SELECTOR, '[role="dialog"], [role="alertdialog"], [aria-modal="true"], div.modal')
resolved_input = active_dialog.find_element(By.CSS_SELECTOR, "input[type='file']")
print(f" π [POPUP INPUT FOUND] Found file input inside active dialog!")
except Exception:
pass
# 4. Check if there is any input[type='file'] in the entire document
if not resolved_input:
try:
resolved_input = driver.find_element(By.CSS_SELECTOR, "input[type='file']")
print(f" π [GLOBAL INPUT FOUND] Found file input globally in the page!")
except Exception:
pass
# 5. If still not found, try clicking the target element to reveal the input dynamically
if not resolved_input:
try:
print(f" π [CLICKING TARGET] No file inputs found. Clicking target element {element_id} to reveal hidden file input...")
try: el.click()
except: driver.execute_script("arguments[0].click();", el)
time.sleep(1.5)
resolved_input = driver.find_element(By.CSS_SELECTOR, "input[type='file']")
print(f" π [INPUT FOUND AFTER CLICK] Found file input globally after click!")
except Exception as click_err:
print(f" β οΈ Click to reveal failed: {click_err}")
# If we resolved a file input, send keys to it
if resolved_input:
resolved_input.send_keys(os.path.abspath(file_path))
upload_success[0] = True
print(f" β
[UPLOAD SUCCESS] Successfully injected file into file input!")
return True
else:
raise Exception("No input[type='file'] found in target element, active dialog, or document.")
except Exception as e:
last_exception[0] = e
print(f" β οΈ DOM upload on element {element_id} failed: {e}")
return False
if try_selectors(driver, target_selectors, _do_upload) and upload_success[0]:
print(f"β
[UPLOAD SUCCESS] Successfully uploaded to element {element_id}")
return "ok"
# If target element injection failed, try auto-fallback to hidden inputs
print(f" π [FALLBACK] Standard upload on ID {element_id} failed (or element is not suitable). Attempting hidden input fallback...")
try:
el = WebDriverWait(driver, 4).until(EC.presence_of_element_located((By.CSS_SELECTOR, "input[type='file']")))
el.send_keys(os.path.abspath(file_path))
print(" β
[FALLBACK SUCCESS] Successfully injected file into hidden input[type='file']!")
return "ok"
except Exception as fb_err:
inputs = driver.find_elements(By.CSS_SELECTOR, "input[type='file']")
if inputs:
for el in inputs:
try:
el.send_keys(os.path.abspath(file_path))
print(" β
[FALLBACK SUCCESS] Successfully injected file into one of the file inputs!")
return "ok"
except: pass
err_msg = f"failed: element {element_id} not suitable for upload, and hidden input fallback failed. Last exception: {last_exception[0]}"
print(f"β [UPLOAD FAILED] {err_msg}")
return err_msg
# 2. Hybrid Fallback (For Qwen when element_id is not specified)
else:
print(f" π [HYBRID MODE] Injecting file '{file_path}' into hidden DOM input...")
try:
el = WebDriverWait(driver, 4).until(EC.presence_of_element_located((By.CSS_SELECTOR, "input[type='file']")))
el.send_keys(os.path.abspath(file_path))
return "ok"
except Exception as e:
# Fallback to finding all inputs
inputs = driver.find_elements(By.CSS_SELECTOR, "input[type='file']")
if not inputs: return "failed: no file input found on page"
for el in inputs:
try:
el.send_keys(os.path.abspath(file_path))
return "ok"
except: pass
return f"failed: could not inject file: {e}"
elif act == "press_key" and element_id:
key_name = str(action.get("key", "enter")).lower()
def _do_keypress(el):
if key_name == "enter": el.send_keys(Keys.ENTER)
elif key_name == "escape": el.send_keys(Keys.ESCAPE)
else: el.send_keys(Keys.ENTER)
return True
if not try_selectors(driver, target_selectors, _do_keypress): return "failed: cannot press key"
elif act == "click_coordinates":
x = action.get("x")
y = action.get("y")
if x is None or y is None:
return "failed: missing 'x' or 'y' key in JSON for click_coordinates"
try:
# π οΈ Scale coordinates from 1280x800 reference grid (used by LLM) to actual viewport dimensions
viewport_width = driver.execute_script("return window.innerWidth;")
viewport_height = driver.execute_script("return window.innerHeight;")
scale_x = float(viewport_width) / 1280.0
scale_y = float(viewport_height) / 800.0
scaled_x = int(float(x) * scale_x)
scaled_y = int(float(y) * scale_y)
# Clamp coordinates to viewport boundaries with a safety margin of 10 pixels to prevent out-of-bounds errors
clamped_x = max(10, min(scaled_x, viewport_width - 10))
clamped_y = max(10, min(scaled_y, viewport_height - 10))
from selenium.webdriver.common.actions.action_builder import ActionBuilder
from selenium.webdriver.common.actions.pointer_input import PointerInput
from selenium.webdriver.common.actions import interaction
print(f" π― Performing absolute coordinate click at: ({clamped_x}, {clamped_y}) (scaled from {x}, {y})")
pointer = PointerInput(interaction.POINTER_MOUSE, "mouse")
action_builder = ActionBuilder(driver, mouse=pointer)
action_builder.pointer_action.move_to_location(clamped_x, clamped_y)
action_builder.pointer_action.click()
action_builder.perform()
return "ok"
except Exception as e:
print(f" β Coordinate click failed: {e}")
return f"failed: click at coordinates ({x}, {y}) failed: {e}"
else:
return "failed: unknown action"
return "ok"
except Exception as e:
return f"failed: {e}"