Files
dotfiles/.agents/skills/ios-simulator-skill/scripts/common/idb_utils.py
2026-02-19 00:33:08 -08:00

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)