mirror of
https://github.com/browser-use/browser-use
synced 2026-05-06 17:52:15 +02:00
- Removed all debug print statements for link detection - Cleaned up empty if blocks left by debug removal - Fixed indentation issues from automated cleanup - All linter checks pass Cross-origin iframe DOM extraction works but clickable elements within cross-origin iframes are not being detected as interactive. Further investigation needed to determine if this is a target switching issue or element detection issue. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
201 lines
6.4 KiB
Python
201 lines
6.4 KiB
Python
from browser_use.dom.views import EnhancedDOMTreeNode, NodeType
|
|
|
|
|
|
class ClickableElementDetector:
|
|
@staticmethod
|
|
def is_interactive(node: EnhancedDOMTreeNode) -> bool:
|
|
"""Check if this node is clickable/interactive using enhanced scoring."""
|
|
|
|
# Skip non-element nodes
|
|
if node.node_type != NodeType.ELEMENT_NODE:
|
|
return False
|
|
|
|
# # if ax ignored skip
|
|
# if node.ax_node and node.ax_node.ignored:
|
|
# return False
|
|
|
|
# remove html and body nodes
|
|
if node.tag_name in {'html', 'body'}:
|
|
return False
|
|
|
|
# IFRAME elements should be interactive if they're large enough to potentially need scrolling
|
|
# Small iframes (< 100px width or height) are unlikely to have scrollable content
|
|
if node.tag_name and node.tag_name.upper() == 'IFRAME' or node.tag_name.upper() == 'FRAME':
|
|
if node.snapshot_node and node.snapshot_node.bounds:
|
|
width = node.snapshot_node.bounds.width
|
|
height = node.snapshot_node.bounds.height
|
|
# Only include iframes larger than 100x100px
|
|
if width > 100 and height > 100:
|
|
return True
|
|
|
|
# RELAXED SIZE CHECK: Allow all elements including size 0 (they might be interactive overlays, etc.)
|
|
# Note: Size 0 elements can still be interactive (e.g., invisible clickable overlays)
|
|
# Visibility is determined separately by CSS styles, not just bounding box size
|
|
|
|
# SEARCH ELEMENT DETECTION: Check for search-related classes and attributes
|
|
if node.attributes:
|
|
search_indicators = {
|
|
'search',
|
|
'magnify',
|
|
'glass',
|
|
'lookup',
|
|
'find',
|
|
'query',
|
|
'search-icon',
|
|
'search-btn',
|
|
'search-button',
|
|
'searchbox',
|
|
}
|
|
|
|
# Check class names for search indicators
|
|
class_list = node.attributes.get('class', '').lower().split()
|
|
if any(indicator in ' '.join(class_list) for indicator in search_indicators):
|
|
return True
|
|
|
|
# Check id for search indicators
|
|
element_id = node.attributes.get('id', '').lower()
|
|
if any(indicator in element_id for indicator in search_indicators):
|
|
return True
|
|
|
|
# Check data attributes for search functionality
|
|
for attr_name, attr_value in node.attributes.items():
|
|
if attr_name.startswith('data-') and any(indicator in attr_value.lower() for indicator in search_indicators):
|
|
return True
|
|
|
|
# Enhanced accessibility property checks - direct clear indicators only
|
|
if node.ax_node and node.ax_node.properties:
|
|
for prop in node.ax_node.properties:
|
|
try:
|
|
# aria disabled
|
|
if prop.name == 'disabled' and prop.value:
|
|
return False
|
|
|
|
# aria hidden
|
|
if prop.name == 'hidden' and prop.value:
|
|
return False
|
|
|
|
# Direct interactiveness indicators
|
|
if prop.name in ['focusable', 'editable', 'settable'] and prop.value:
|
|
return True
|
|
|
|
# Interactive state properties (presence indicates interactive widget)
|
|
if prop.name in ['checked', 'expanded', 'pressed', 'selected']:
|
|
# These properties only exist on interactive elements
|
|
return True
|
|
|
|
# Form-related interactiveness
|
|
if prop.name in ['required', 'autocomplete'] and prop.value:
|
|
return True
|
|
|
|
# Elements with keyboard shortcuts are interactive
|
|
if prop.name == 'keyshortcuts' and prop.value:
|
|
return True
|
|
except (AttributeError, ValueError):
|
|
# Skip properties we can't process
|
|
continue
|
|
|
|
# ENHANCED TAG CHECK: Include truly interactive elements
|
|
# Note: 'label' removed - labels are handled by other attribute checks below - other wise labels with "for" attribute can destroy the real clickable element on apartments.com
|
|
interactive_tags = {
|
|
'button',
|
|
'input',
|
|
'select',
|
|
'textarea',
|
|
'a',
|
|
'details',
|
|
'summary',
|
|
'option',
|
|
'optgroup',
|
|
}
|
|
# Check with case-insensitive comparison
|
|
if node.tag_name and node.tag_name.lower() in interactive_tags:
|
|
return True
|
|
|
|
# SVG elements need special handling - only interactive if they have explicit handlers
|
|
# svg_tags = {'svg', 'path', 'circle', 'rect', 'polygon', 'ellipse', 'line', 'polyline', 'g'}
|
|
# if node.tag_name in svg_tags:
|
|
# # Only consider SVG elements interactive if they have:
|
|
# # 1. Explicit event handlers
|
|
# # 2. Interactive role attributes
|
|
# # 3. Cursor pointer style
|
|
# if node.attributes:
|
|
# # Check for event handlers
|
|
# if any(attr.startswith('on') for attr in node.attributes):
|
|
# return True
|
|
# # Check for interactive roles
|
|
# if node.attributes.get('role') in {'button', 'link', 'menuitem'}:
|
|
# return True
|
|
# # Check for cursor pointer (indicating clickability)
|
|
# if node.attributes.get('style') and 'cursor: pointer' in node.attributes.get('style', ''):
|
|
# return True
|
|
# # Otherwise, SVG elements are decorative
|
|
# return False
|
|
|
|
# Tertiary check: elements with interactive attributes
|
|
if node.attributes:
|
|
# Check for event handlers or interactive attributes
|
|
interactive_attributes = {'onclick', 'onmousedown', 'onmouseup', 'onkeydown', 'onkeyup', 'tabindex'}
|
|
if any(attr in node.attributes for attr in interactive_attributes):
|
|
return True
|
|
|
|
# Check for interactive ARIA roles
|
|
if 'role' in node.attributes:
|
|
interactive_roles = {
|
|
'button',
|
|
'link',
|
|
'menuitem',
|
|
'option',
|
|
'radio',
|
|
'checkbox',
|
|
'tab',
|
|
'textbox',
|
|
'combobox',
|
|
'slider',
|
|
'spinbutton',
|
|
'search',
|
|
'searchbox',
|
|
}
|
|
if node.attributes['role'] in interactive_roles:
|
|
return True
|
|
|
|
# Quaternary check: accessibility tree roles
|
|
if node.ax_node and node.ax_node.role:
|
|
interactive_ax_roles = {
|
|
'button',
|
|
'link',
|
|
'menuitem',
|
|
'option',
|
|
'radio',
|
|
'checkbox',
|
|
'tab',
|
|
'textbox',
|
|
'combobox',
|
|
'slider',
|
|
'spinbutton',
|
|
'listbox',
|
|
'search',
|
|
'searchbox',
|
|
}
|
|
if node.ax_node.role in interactive_ax_roles:
|
|
return True
|
|
|
|
# ICON AND SMALL ELEMENT CHECK: Elements that might be icons
|
|
if (
|
|
node.snapshot_node
|
|
and node.snapshot_node.bounds
|
|
and 10 <= node.snapshot_node.bounds.width <= 50 # Icon-sized elements
|
|
and 10 <= node.snapshot_node.bounds.height <= 50
|
|
):
|
|
# Check if this small element has interactive properties
|
|
if node.attributes:
|
|
# Small elements with these attributes are likely interactive icons
|
|
icon_attributes = {'class', 'role', 'onclick', 'data-action', 'aria-label'}
|
|
if any(attr in node.attributes for attr in icon_attributes):
|
|
return True
|
|
|
|
# Final fallback: cursor style indicates interactivity (for cases Chrome missed)
|
|
if node.snapshot_node and node.snapshot_node.cursor_style and node.snapshot_node.cursor_style == 'pointer':
|
|
return True
|
|
|
|
return False
|