ITP WiP
This commit is contained in:
0
tools/itb/bin/itb_click
Executable file
0
tools/itb/bin/itb_click
Executable file
0
tools/itb/bin/itb_forms
Executable file
0
tools/itb/bin/itb_forms
Executable file
0
tools/itb/bin/itb_input
Executable file
0
tools/itb/bin/itb_input
Executable file
0
tools/itb/bin/itb_links
Executable file
0
tools/itb/bin/itb_links
Executable file
0
tools/itb/bin/itb_navigate
Executable file
0
tools/itb/bin/itb_navigate
Executable file
0
tools/itb/bin/itb_refresh
Executable file
0
tools/itb/bin/itb_refresh
Executable file
180
tools/itb/bin/itb_screenshot
Executable file
180
tools/itb/bin/itb_screenshot
Executable file
@@ -0,0 +1,180 @@
|
||||
#!/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
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass
|
||||
class DOMNode:
|
||||
element: WebElement
|
||||
children: List['DOMNode']
|
||||
own_text: str
|
||||
total_text: str
|
||||
tag: str
|
||||
location: Dict[str, int]
|
||||
size: Dict[str, int]
|
||||
is_interactive: bool
|
||||
attributes: Dict[str, str]
|
||||
|
||||
class DOMTree:
|
||||
def __init__(self, root_element: WebElement):
|
||||
self.root = self._build_tree(root_element)
|
||||
|
||||
def _get_element_own_text(self, element: WebElement) -> str:
|
||||
script = """
|
||||
let element = arguments[0];
|
||||
let walker = document.createTreeWalker(
|
||||
element,
|
||||
NodeFilter.SHOW_TEXT,
|
||||
{
|
||||
acceptNode: function(node) {
|
||||
return node.parentElement === element ?
|
||||
NodeFilter.FILTER_ACCEPT :
|
||||
NodeFilter.FILTER_REJECT;
|
||||
}
|
||||
}
|
||||
);
|
||||
let text = '';
|
||||
let node;
|
||||
while (node = walker.nextNode()) {
|
||||
text += node.textContent;
|
||||
}
|
||||
return text.trim();
|
||||
"""
|
||||
return element.parent.execute_script(script, element)
|
||||
|
||||
def _is_interactive(self, element: WebElement) -> bool:
|
||||
return (
|
||||
element.tag_name in {"button", "a", "input", "select", "textarea"} or
|
||||
element.get_attribute("role") in {"button", "link", "textbox"} or
|
||||
element.get_attribute("onclick") is not None or
|
||||
element.get_attribute("contenteditable") == "true"
|
||||
)
|
||||
|
||||
def _get_attributes(self, element: WebElement) -> Dict[str, str]:
|
||||
attrs = {}
|
||||
for attr in ['placeholder', 'aria-label', 'alt', 'title', 'role']:
|
||||
if value := element.get_attribute(attr):
|
||||
attrs[attr] = value
|
||||
return attrs
|
||||
|
||||
def _build_tree(self, element: WebElement) -> DOMNode:
|
||||
children = [
|
||||
self._build_tree(child)
|
||||
for child in element.find_elements(By.XPATH, "./*")
|
||||
]
|
||||
|
||||
own_text = self._get_element_own_text(element)
|
||||
total_text = own_text + " " + " ".join(
|
||||
child.total_text for child in children
|
||||
)
|
||||
total_text = " ".join(total_text.split())
|
||||
|
||||
return DOMNode(
|
||||
element=element,
|
||||
children=children,
|
||||
own_text=own_text,
|
||||
total_text=total_text,
|
||||
tag=element.tag_name,
|
||||
location=element.location,
|
||||
size=element.size,
|
||||
is_interactive=self._is_interactive(element),
|
||||
attributes=self._get_attributes(element)
|
||||
)
|
||||
|
||||
class ScreenshotGenerator:
|
||||
IGNORED_TAGS = {'script', 'style', 'meta', 'link', 'noscript'}
|
||||
|
||||
def __init__(self, debug_port: int):
|
||||
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, int]:
|
||||
return {
|
||||
"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);"
|
||||
)
|
||||
}
|
||||
|
||||
def should_process_node(self, node: DOMNode) -> bool:
|
||||
# Skip invisible elements
|
||||
if not node.element.is_displayed():
|
||||
return False
|
||||
|
||||
# Skip script and style elements
|
||||
if node.tag in self.IGNORED_TAGS:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def create_xml_element(self, node: DOMNode, parent_element: Optional[ET.Element] = None) -> Optional[ET.Element]:
|
||||
attrs = {
|
||||
"x": str(node.location["x"]),
|
||||
"y": str(node.location["y"]),
|
||||
"width": str(node.size["width"]),
|
||||
"height": str(node.size["height"])
|
||||
}
|
||||
|
||||
if node.is_interactive:
|
||||
element = ET.Element("interactive", attrs)
|
||||
child = ET.SubElement(element, node.tag)
|
||||
for attr, value in node.attributes.items():
|
||||
child.set(attr, value)
|
||||
if node.own_text:
|
||||
child.text = node.own_text
|
||||
return element
|
||||
|
||||
element = ET.Element(node.tag, attrs)
|
||||
if node.own_text:
|
||||
element.text = node.own_text
|
||||
return element
|
||||
|
||||
def process_tree(self, node: DOMNode, parent_element: Optional[ET.Element] = None) -> List[ET.Element]:
|
||||
if not self.should_process_node(node):
|
||||
return []
|
||||
|
||||
current_element = self.create_xml_element(node, parent_element)
|
||||
if not current_element:
|
||||
return []
|
||||
|
||||
for child in node.children:
|
||||
child_elements = self.process_tree(child, current_element)
|
||||
for child_element in child_elements:
|
||||
current_element.append(child_element)
|
||||
|
||||
return [current_element]
|
||||
|
||||
def generate_output(self) -> ET.Element:
|
||||
metrics = self.get_viewport_metrics()
|
||||
viewport = ET.Element("viewport", {
|
||||
"scroll_x": str(metrics["scroll_x"]),
|
||||
"scroll_y": str(metrics["scroll_y"]),
|
||||
"width": str(metrics["width"]),
|
||||
"height": str(metrics["height"]),
|
||||
"max_scroll_y": str(metrics["max_scroll_y"])
|
||||
})
|
||||
|
||||
dom_tree = DOMTree(self.driver.find_element(By.TAG_NAME, "body"))
|
||||
for element in self.process_tree(dom_tree.root):
|
||||
viewport.append(element)
|
||||
|
||||
return viewport
|
||||
|
||||
def main(debug_port: int):
|
||||
generator = ScreenshotGenerator(debug_port)
|
||||
output = generator.generate_output()
|
||||
print(ET.tostring(output, encoding="unicode", method="xml"))
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 2:
|
||||
print("Usage: itb_screenshot <debug_port>")
|
||||
sys.exit(1)
|
||||
main(int(sys.argv[1]))
|
||||
0
tools/itb/bin/itb_scroll
Executable file
0
tools/itb/bin/itb_scroll
Executable file
33
tools/itb/bin/itb_start
Executable file
33
tools/itb/bin/itb_start
Executable file
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env python3
|
||||
import random
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
def start(url):
|
||||
debug_port = random.randint(9000, 9999)
|
||||
chrome_cmd = [
|
||||
'/usr/bin/google-chrome',
|
||||
'--headless=new',
|
||||
'--no-sandbox',
|
||||
'--remote-debugging-port=' + str(debug_port),
|
||||
url
|
||||
]
|
||||
|
||||
process = subprocess.Popen(
|
||||
chrome_cmd,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
start_new_session=True
|
||||
)
|
||||
|
||||
print({
|
||||
"pid": process.pid,
|
||||
"debug_port": debug_port
|
||||
})
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) != 2:
|
||||
print("Usage: itb_start <url>")
|
||||
sys.exit(1)
|
||||
start(sys.argv[1])
|
||||
Reference in New Issue
Block a user