"""Tests for :mod:`python_pkg.android_ui.driver`. The cases mirror failures actually observed driving a Pixel 6a on 2026-08-10, because each of them looked like success at the time: * a tap that landed on an unfocused field, so the typed text went nowhere; * a widget behind the soft keyboard whose reported position was 390px off; * ``uiautomator dump`` returning only the focused ``EditText`` nodes; * ``input text`` appending to a filled field instead of replacing it. """ from __future__ import annotations from typing import NamedTuple import pytest from python_pkg.android_ui import driver as drv from python_pkg.android_ui.tests.conftest import FakeDevice from python_pkg.android_ui.tests.conftest import node as _node from python_pkg.android_ui.tests.conftest import tree as _tree MOD = "python_pkg.android_ui.driver" class Harness(NamedTuple): """A driver plus the fake device backing it. Returned as a pair rather than by bolting the device onto the driver, so the test needs no attribute that production code does not have. """ ui: drv.AndroidUi device: FakeDevice @pytest.fixture def ui(monkeypatch: pytest.MonkeyPatch) -> Harness: """An :class:`AndroidUi` wired to a :class:`FakeDevice`.""" device = FakeDevice() instance = drv.AndroidUi(settle_seconds=0.0) monkeypatch.setattr(instance, "_run", device.run) monkeypatch.setattr(f"{MOD}.time.sleep", lambda _s: None) return Harness(ui=instance, device=device) class TestParseTree: def test_keeps_labelled_nodes(self) -> None: elements = drv._parse_tree(_tree(_node("Connect Firebase"))) assert [e.text for e in elements] == ["Connect Firebase"] def test_keeps_empty_edit_text(self) -> None: # An empty field has no text, content-desc or resource-id, yet it is # exactly the element a caller must address in order to fill it in. xml = _tree(_node(cls="android.widget.EditText")) assert len(drv._parse_tree(xml)) == 1 def test_drops_unlabelled_non_editable_nodes(self) -> None: assert drv._parse_tree(_tree(_node())) == [] def test_drops_nodes_without_bounds(self) -> None: xml = _tree('') assert drv._parse_tree(xml) == [] def test_malformed_xml_is_an_empty_screen_not_a_crash(self) -> None: # A dump taken mid-animation is truncated; callers retry rather than die. assert drv._parse_tree(" None: element = drv._parse_tree(_tree(_node("A", bounds="[10,20][30,60]")))[0] assert element.center == (20, 40) def test_label_prefers_text_then_desc_then_id(self) -> None: by_desc = drv._parse_tree(_tree(_node(desc="described")))[0] by_id = drv._parse_tree(_tree(_node(res="com.x:id/y")))[0] assert by_desc.label == "described" assert by_id.label == "com.x:id/y" def test_substring_match_is_case_insensitive(self) -> None: element = drv._parse_tree(_tree(_node("Connect Firebase")))[0] assert element.matches("connect") assert not element.matches("connect", exact=True) assert element.matches("Connect Firebase", exact=True) def test_str_names_the_element_and_its_centre(self) -> None: element = drv._parse_tree( _tree(_node("Go", cls="android.widget.Button", bounds="[0,0][10,10]")) )[0] assert str(element) == "'Go'