mirror of
https://github.com/ksyasuda/dotfiles.git
synced 2026-02-28 00:22:41 -08:00
181 lines
5.3 KiB
Python
181 lines
5.3 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Shared IDB utility functions.
|
|
|
|
This module provides common IDB operations used across multiple scripts.
|
|
Follows Jackson's Law - only shared code that's truly reused, not speculative.
|
|
|
|
Used by:
|
|
- navigator.py - Accessibility tree navigation
|
|
- screen_mapper.py - UI element analysis
|
|
- accessibility_audit.py - WCAG compliance checking
|
|
- test_recorder.py - Test documentation
|
|
- app_state_capture.py - State snapshots
|
|
- gesture.py - Touch gesture operations
|
|
"""
|
|
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
|
|
|
|
def get_accessibility_tree(udid: str | None = None, nested: bool = True) -> dict:
|
|
"""
|
|
Fetch accessibility tree from IDB.
|
|
|
|
The accessibility tree represents the complete UI hierarchy of the current
|
|
screen, with all element properties needed for semantic navigation.
|
|
|
|
Args:
|
|
udid: Device UDID (uses booted simulator if None)
|
|
nested: Include nested structure (default True). If False, returns flat array.
|
|
|
|
Returns:
|
|
Root element of accessibility tree as dict.
|
|
Structure: {
|
|
"type": "Window",
|
|
"AXLabel": "App Name",
|
|
"frame": {"x": 0, "y": 0, "width": 390, "height": 844},
|
|
"children": [...]
|
|
}
|
|
|
|
Raises:
|
|
SystemExit: If IDB command fails or returns invalid JSON
|
|
|
|
Example:
|
|
tree = get_accessibility_tree("UDID123")
|
|
# Root is Window element with all children nested
|
|
"""
|
|
cmd = ["idb", "ui", "describe-all", "--json"]
|
|
if nested:
|
|
cmd.append("--nested")
|
|
if udid:
|
|
cmd.extend(["--udid", udid])
|
|
|
|
try:
|
|
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
|
tree_data = json.loads(result.stdout)
|
|
|
|
# IDB returns array format, extract first element (root)
|
|
if isinstance(tree_data, list) and len(tree_data) > 0:
|
|
return tree_data[0]
|
|
return tree_data
|
|
except subprocess.CalledProcessError as e:
|
|
print(f"Error: Failed to get accessibility tree: {e.stderr}", file=sys.stderr)
|
|
sys.exit(1)
|
|
except json.JSONDecodeError:
|
|
print("Error: Invalid JSON from idb", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
|
|
def flatten_tree(node: dict, depth: int = 0, elements: list[dict] | None = None) -> list[dict]:
|
|
"""
|
|
Flatten nested accessibility tree into list of elements.
|
|
|
|
Converts the hierarchical accessibility tree into a flat list where each
|
|
element includes its depth for context.
|
|
|
|
Used by:
|
|
- navigator.py - Element finding
|
|
- screen_mapper.py - Element analysis
|
|
- accessibility_audit.py - Audit scanning
|
|
|
|
Args:
|
|
node: Root node of tree (typically from get_accessibility_tree)
|
|
depth: Current depth (used internally, start at 0)
|
|
elements: Accumulator list (used internally, start as None)
|
|
|
|
Returns:
|
|
Flat list of elements, each with "depth" key indicating nesting level.
|
|
Structure of each element: {
|
|
"type": "Button",
|
|
"AXLabel": "Login",
|
|
"frame": {...},
|
|
"depth": 2,
|
|
...
|
|
}
|
|
|
|
Example:
|
|
tree = get_accessibility_tree()
|
|
flat = flatten_tree(tree)
|
|
for elem in flat:
|
|
print(f"{' ' * elem['depth']}{elem.get('type')}: {elem.get('AXLabel')}")
|
|
"""
|
|
if elements is None:
|
|
elements = []
|
|
|
|
# Add current node with depth tracking
|
|
node_copy = node.copy()
|
|
node_copy["depth"] = depth
|
|
elements.append(node_copy)
|
|
|
|
# Process children recursively
|
|
for child in node.get("children", []):
|
|
flatten_tree(child, depth + 1, elements)
|
|
|
|
return elements
|
|
|
|
|
|
def count_elements(node: dict) -> int:
|
|
"""
|
|
Count total elements in tree (recursive).
|
|
|
|
Traverses entire tree counting all elements for reporting purposes.
|
|
|
|
Used by:
|
|
- test_recorder.py - Element counting per step
|
|
- screen_mapper.py - Summary statistics
|
|
|
|
Args:
|
|
node: Root node of tree
|
|
|
|
Returns:
|
|
Total element count including root and all descendants
|
|
|
|
Example:
|
|
tree = get_accessibility_tree()
|
|
total = count_elements(tree)
|
|
print(f"Screen has {total} elements")
|
|
"""
|
|
count = 1
|
|
for child in node.get("children", []):
|
|
count += count_elements(child)
|
|
return count
|
|
|
|
|
|
def get_screen_size(udid: str | None = None) -> tuple[int, int]:
|
|
"""
|
|
Get screen dimensions from accessibility tree.
|
|
|
|
Extracts the screen size from the root element's frame. Useful for
|
|
gesture calculations and coordinate normalization.
|
|
|
|
Used by:
|
|
- gesture.py - Gesture positioning
|
|
- Potentially: screenshot positioning, screen-aware scaling
|
|
|
|
Args:
|
|
udid: Device UDID (uses booted if None)
|
|
|
|
Returns:
|
|
(width, height) tuple. Defaults to (390, 844) if detection fails
|
|
or tree cannot be accessed.
|
|
|
|
Example:
|
|
width, height = get_screen_size()
|
|
center_x = width // 2
|
|
center_y = height // 2
|
|
"""
|
|
DEFAULT_WIDTH = 390 # iPhone 14
|
|
DEFAULT_HEIGHT = 844
|
|
|
|
try:
|
|
tree = get_accessibility_tree(udid, nested=False)
|
|
frame = tree.get("frame", {})
|
|
width = int(frame.get("width", DEFAULT_WIDTH))
|
|
height = int(frame.get("height", DEFAULT_HEIGHT))
|
|
return (width, height)
|
|
except Exception:
|
|
# Silently fall back to defaults if tree access fails
|
|
return (DEFAULT_WIDTH, DEFAULT_HEIGHT)
|