#!/usr/bin/env python3 import sys from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.remote.webelement import WebElement import xml.etree.ElementTree as ET from typing import Dict, List, Set, Optional, Union from dataclasses import dataclass @dataclass class DOMNode: element: WebElement children: List['DOMNode'] text: str tag: str location: Dict[str, int] size: Dict[str, int] interactions: List[str] def get_content(self) -> List[Union[ET.Element, str]]: content = [] # Add text as plain string if present if self.text: content.append(self.text) # Add interactive element if present if self.interactions: attrs = { "x": str(self.location["x"]), "y": str(self.location["y"]), "width": str(self.size["width"]), "height": str(self.size["height"]) } # Split interactions and placeholders base_interactions = [] placeholder = None for interaction in self.interactions: if interaction.startswith("placeholder:"): placeholder = interaction.split(":", 1)[1] else: base_interactions.append(interaction) attrs["interactions"] = ",".join(base_interactions) if placeholder: attrs["placeholder"] = placeholder interactive = ET.Element("interactive", attrs) if self.text: interactive.text = self.text content.append(interactive) # Add all children's content for child in self.children: content.extend(child.get_content()) return content class DOMTree: def __init__(self, root_element: WebElement): print(f"\nBuilding DOM tree for {root_element.tag_name}") self.root = self._build_tree(root_element) def _get_element_text(self, element: WebElement) -> str: script = """ let element = arguments[0]; let text = ''; for (let node of element.childNodes) { if (node.nodeType === Node.TEXT_NODE) { text += node.textContent; } } return text.trim(); """ text = element.parent.execute_script(script, element) if text: print(f"Got text: {text[:50]}...") return text def _get_interactions(self, element: WebElement) -> List[str]: interactions = [] if (element.tag_name in {"button", "a"} or element.get_attribute("role") in {"button", "link"} or element.get_attribute("onclick") or element.value_of_css_property("cursor") == "pointer"): interactions.append("click") if (element.tag_name in {"input", "textarea"} or element.get_attribute("contenteditable") == "true" or element.get_attribute("role") == "textbox"): interactions.append("text_input") # Get placeholder if available placeholder = element.get_attribute("placeholder") if placeholder: interactions.append(f"placeholder:{placeholder}") if (element.tag_name == "select" or element.get_attribute("role") == "listbox" or element.get_attribute("type") in {"checkbox", "radio"}): interactions.append("select") script = """ let el = arguments[0]; return el.scrollHeight > el.clientHeight || el.scrollWidth > el.clientWidth; """ if element.parent.execute_script(script, element): interactions.append("scroll") if interactions: print(f"Found interactions for {element.tag_name}: {interactions}") return interactions def _build_tree(self, element: WebElement) -> DOMNode: print(f"\nProcessing <{element.tag_name}>") if not element.is_displayed(): print(f"Skipping invisible {element.tag_name}") return None children = [] for child in element.find_elements(By.XPATH, "./*"): if child_node := self._build_tree(child): children.append(child_node) text = self._get_element_text(element) interactions = self._get_interactions(element) return DOMNode( element=element, children=children, text=text, tag=element.tag_name, location=element.location, size=element.size, interactions=interactions ) class ScreenshotGenerator: def __init__(self, debug_port: int): print(f"\nInitializing ScreenshotGenerator with port {debug_port}") options = webdriver.ChromeOptions() options.add_experimental_option("debuggerAddress", f"127.0.0.1:{debug_port}") self.driver = webdriver.Chrome(options=options) def get_viewport_metrics(self) -> Dict[str, str]: metrics = { "scroll_x": self.driver.execute_script("return window.pageXOffset;"), "scroll_y": self.driver.execute_script("return window.pageYOffset;"), "width": self.driver.execute_script("return document.documentElement.clientWidth;"), "height": self.driver.execute_script("return document.documentElement.clientHeight;"), "max_scroll_y": self.driver.execute_script( "return Math.max(document.documentElement.scrollHeight - window.innerHeight, 0);" ), "min_scroll_x": self.driver.execute_script( "return Math.max(document.documentElement.scrollLeft, 0);" ) } print(f"\nViewport metrics: {metrics}") return {k: str(v) for k, v in metrics.items()} def generate_xml(self) -> str: metrics = self.get_viewport_metrics() metrics_str = " ".join(f'{k}="{v}"' for k, v in metrics.items()) output = [f''] seen_texts = set() last_was_text = False body = self.driver.find_element(By.TAG_NAME, "body") dom_tree = DOMTree(body) for content in dom_tree.root.get_content(): if isinstance(content, str): if content not in seen_texts: if last_was_text: output.append("\n") output.append(content) seen_texts.add(content) last_was_text = True else: output.append(ET.tostring(content, encoding="unicode")) last_was_text = False output.append('') return "".join(output) def main(debug_port: int): generator = ScreenshotGenerator(debug_port) print("\nFinal XML output:") print(generator.generate_xml()) if __name__ == "__main__": if len(sys.argv) != 2: print("Usage: itb_screenshot ") sys.exit(1) main(int(sys.argv[1]))