from __future__ import annotations from pathlib import Path from core.errors import DriverError from core.models import Bounds from device.manager import DeviceManager from driver.base import Driver from perception.ocr import OCRBox from tools.describe_screen import describe_screen PNG_10X20 = b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x0a\x00\x00\x00\x14" class FakeOCREngine: def __init__(self, boxes: list[OCRBox]) -> None: self._boxes = boxes def extract(self, image: bytes | str | Path) -> list[OCRBox]: return self._boxes class TreeFailingDriver(Driver): def connect(self) -> None: return None def disconnect(self) -> None: return None def screenshot(self) -> bytes: return PNG_10X20 def tap(self, x: float, y: float) -> None: return None def long_press(self, x: float, y: float, duration_ms: int = 1200) -> None: return None def swipe( self, start_x: float, start_y: float, end_x: float, end_y: float, duration_ms: int = 500, ) -> None: return None def input(self, text: str) -> None: return None def launch(self, app_id: str) -> None: return None def terminate(self, app_id: str) -> None: return None def tree(self): raise DriverError("ui tree retrieval failed") def home(self) -> None: return None def lock(self) -> None: return None def unlock(self) -> None: return None def test_describe_screen_degrades_to_ocr_only_when_ui_tree_unavailable() -> None: manager = DeviceManager() manager.register_device("phone-1", lambda: TreeFailingDriver()) manager.connect("phone-1") fake_box = OCRBox(text="Search", bounds=Bounds(1, 2, 4, 4), confidence=0.9) fake_engine = FakeOCREngine([fake_box]) scene = describe_screen( "phone-1", manager=manager, ocr_engine=fake_engine, # type: ignore[arg-type] ) assert [element.text for element in scene.elements] == ["Search"] assert all(element.source == "ocr" for element in scene.elements)