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.
142 lines
5.0 KiB
Python
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
|