Files
SIA/tools/itb/bin/itb_screenshot
Niels Geens 6ab61ed384 ITB WIP
2024-12-08 09:51:43 +01:00

201 lines
7.2 KiB
Python
Executable File

#!/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'<viewport {metrics_str}>']
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('</viewport>')
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 <debug_port>")
sys.exit(1)
main(int(sys.argv[1]))