Spaces:
Sleeping
Sleeping
| from browser_use.dom.views import EnhancedDOMTreeNode, NodeType | |
| class ClickableElementDetector: | |
| 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 | |