mcp-adb/tests/test_ui.py
Ryan Malloy 3614ba8f8f Replace dict returns with typed Pydantic response models across all 65 tools
Every tool now returns a structured BaseModel instead of dict[str, Any],
giving callers attribute access, IDE autocomplete, and schema validation.
Adds ~30 model classes to models.py and updates all test assertions.
2026-02-11 03:57:25 -07:00

142 lines
5.0 KiB
Python

"""Tests for UI inspection mixin (dump, find, wait, tap_text)."""
from tests.conftest import fail, ok
SAMPLE_UI_XML = """<?xml version="1.0" encoding="UTF-8"?>
<hierarchy>
<node text="Settings" class="android.widget.TextView"
resource-id="com.android.settings:id/title"
bounds="[0,100][200,150]" clickable="true" focusable="true"
content-desc="" />
<node text="" class="android.widget.ImageView"
resource-id="com.android.settings:id/icon"
bounds="[0,50][48,98]" clickable="false" focusable="false"
content-desc="Settings icon" />
<node text="Wi-Fi" class="android.widget.TextView"
resource-id="com.android.settings:id/title"
bounds="[200,100][400,150]" clickable="true" focusable="false"
content-desc="" />
</hierarchy>
"""
class TestUiDump:
async def test_dump(self, server, ctx):
server.run_shell_args.side_effect = [
ok(), # uiautomator dump
ok(stdout=SAMPLE_UI_XML), # cat
ok(), # rm cleanup
]
result = await server.ui_dump(ctx)
assert result.success is True
assert result.element_count >= 2 # Settings + Wi-Fi at minimum
assert result.xml is not None
async def test_dump_failure(self, server, ctx):
server.run_shell_args.return_value = fail("error")
result = await server.ui_dump(ctx)
assert result.success is False
class TestParseUiElements:
def test_parse_clickable(self, server):
elements = server._parse_ui_elements(SAMPLE_UI_XML)
texts = [e["text"] for e in elements]
assert "Settings" in texts
assert "Wi-Fi" in texts
def test_center_coordinates(self, server):
elements = server._parse_ui_elements(SAMPLE_UI_XML)
settings = [e for e in elements if e["text"] == "Settings"][0]
assert settings["center"] == {"x": 100, "y": 125}
def test_content_desc_included(self, server):
elements = server._parse_ui_elements(SAMPLE_UI_XML)
icon = [e for e in elements if e["content_desc"] == "Settings icon"]
assert len(icon) == 1
def test_empty_xml(self, server):
elements = server._parse_ui_elements("")
assert elements == []
class TestUiFindElement:
async def test_find_by_text(self, server):
server.run_shell_args.side_effect = [ok(), ok(stdout=SAMPLE_UI_XML), ok()]
result = await server.ui_find_element(text="Settings")
assert result.success is True
assert result.count == 1
assert result.matches[0]["text"] == "Settings"
async def test_find_by_resource_id(self, server):
server.run_shell_args.side_effect = [ok(), ok(stdout=SAMPLE_UI_XML), ok()]
result = await server.ui_find_element(resource_id="title")
# Settings and Wi-Fi both have "title" in their resource-id
assert result.count >= 2
async def test_not_found(self, server):
server.run_shell_args.side_effect = [ok(), ok(stdout=SAMPLE_UI_XML), ok()]
result = await server.ui_find_element(text="Missing")
assert result.success is True
assert result.count == 0
class TestWaitForText:
async def test_found_immediately(self, server):
server.run_shell_args.side_effect = [ok(), ok(stdout=SAMPLE_UI_XML), ok()]
result = await server.wait_for_text("Settings", timeout_seconds=1)
assert result.success is True
assert result.found is True
assert result.attempts == 1
async def test_timeout(self, server):
server.run_shell_args.side_effect = [
ok(),
ok(stdout="<hierarchy></hierarchy>"),
ok(),
] * 10
result = await server.wait_for_text(
"Missing", timeout_seconds=0.1, poll_interval=0.05
)
assert result.success is False
assert result.found is False
class TestWaitForTextGone:
async def test_already_gone(self, server):
server.run_shell_args.side_effect = [
ok(),
ok(stdout="<hierarchy></hierarchy>"),
ok(),
]
result = await server.wait_for_text_gone("Missing", timeout_seconds=1)
assert result.success is True
assert result.gone is True
class TestTapText:
async def test_tap_found(self, server):
# find_element calls ui_dump which has 3 calls, then tap has 1
server.run_shell_args.side_effect = [
ok(),
ok(stdout=SAMPLE_UI_XML),
ok(), # ui_dump for find
ok(), # tap
]
result = await server.tap_text("Settings")
assert result.success is True
assert result.coordinates == {"x": 100, "y": 125}
async def test_not_found(self, server):
server.run_shell_args.side_effect = [
ok(),
ok(stdout=SAMPLE_UI_XML),
ok(), # first search by text
ok(),
ok(stdout=SAMPLE_UI_XML),
ok(), # fallback search by content_desc
]
result = await server.tap_text("NonExistent")
assert result.success is False
assert "No element found" in result.error