mcp-adb/tests/test_input.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

237 lines
8.7 KiB
Python

"""Tests for input mixin (tap, swipe, keys, text, clipboard)."""
import pytest
from tests.conftest import fail, ok
class TestInputTap:
async def test_tap(self, server):
server.run_shell_args.return_value = ok()
result = await server.input_tap(100, 200)
assert result.success is True
assert result.coordinates == {"x": 100, "y": 200}
server.run_shell_args.assert_called_once_with(
["input", "tap", "100", "200"], None
)
async def test_tap_with_device(self, server):
server.run_shell_args.return_value = ok()
await server.input_tap(10, 20, device_id="ABC")
server.run_shell_args.assert_called_once_with(
["input", "tap", "10", "20"], "ABC"
)
async def test_tap_failure(self, server):
server.run_shell_args.return_value = fail("no device")
result = await server.input_tap(0, 0)
assert result.success is False
assert result.error == "no device"
class TestInputSwipe:
async def test_swipe(self, server):
server.run_shell_args.return_value = ok()
result = await server.input_swipe(0, 100, 0, 500, duration_ms=500)
assert result.success is True
assert result.start == {"x": 0, "y": 100}
assert result.end == {"x": 0, "y": 500}
assert result.duration_ms == 500
async def test_swipe_default_duration(self, server):
server.run_shell_args.return_value = ok()
result = await server.input_swipe(0, 0, 100, 100)
assert result.duration_ms == 300
class TestInputScroll:
async def test_scroll_down(self, server):
# First call: wm size, second call: the swipe
server.run_shell_args.side_effect = [
ok("Physical size: 1080x1920"),
ok(),
]
result = await server.input_scroll_down()
assert result.success is True
assert result.action == "scroll_down"
# Verify swipe args: center x, 65% down to 25% down
swipe_call = server.run_shell_args.call_args_list[1]
args = swipe_call[0][0]
assert args[0] == "input"
assert args[1] == "swipe"
assert args[2] == "540" # 1080 // 2
assert args[3] == "1248" # int(1920 * 0.65)
assert args[5] == "480" # int(1920 * 0.25)
async def test_scroll_up(self, server):
server.run_shell_args.side_effect = [
ok("Physical size: 1080x1920"),
ok(),
]
result = await server.input_scroll_up()
assert result.success is True
assert result.action == "scroll_up"
async def test_scroll_fallback_dimensions(self, server):
# wm size fails, should fall back to 1080x1920
server.run_shell_args.side_effect = [
fail("error"),
ok(),
]
result = await server.input_scroll_down()
assert result.success is True
class TestInputKeys:
async def test_back(self, server):
server.run_shell_args.return_value = ok()
result = await server.input_back()
assert result.action == "back"
server.run_shell_args.assert_called_once_with(
["input", "keyevent", "KEYCODE_BACK"], None
)
async def test_home(self, server):
server.run_shell_args.return_value = ok()
result = await server.input_home()
assert result.action == "home"
async def test_recent_apps(self, server):
server.run_shell_args.return_value = ok()
result = await server.input_recent_apps()
assert result.action == "recent_apps"
class TestInputKey:
async def test_full_keycode(self, server):
server.run_shell_args.return_value = ok()
result = await server.input_key("KEYCODE_ENTER")
assert result.key_code == "KEYCODE_ENTER"
async def test_auto_prefix(self, server):
server.run_shell_args.return_value = ok()
result = await server.input_key("ENTER")
assert result.key_code == "KEYCODE_ENTER"
async def test_strips_dangerous_chars(self, server):
server.run_shell_args.return_value = ok()
result = await server.input_key("KEYCODE_ENTER; rm -rf /")
# Shell metacharacters stripped
assert ";" not in result.key_code
assert " " not in result.key_code
assert "-" not in result.key_code
assert "/" not in result.key_code
async def test_lowercase_normalized(self, server):
server.run_shell_args.return_value = ok()
result = await server.input_key("enter")
assert result.key_code == "KEYCODE_ENTER"
class TestInputText:
async def test_simple_text(self, server):
server.run_shell_args.return_value = ok()
result = await server.input_text("hello")
assert result.success is True
assert result.text == "hello"
server.run_shell_args.assert_called_once_with(["input", "text", "hello"], None)
async def test_spaces_escaped(self, server):
server.run_shell_args.return_value = ok()
await server.input_text("hello world")
server.run_shell_args.assert_called_once_with(
["input", "text", "hello%sworld"], None
)
async def test_rejects_special_chars(self, server):
for char in "'\"\\`$(){}[]|&;<>!~#%^*?":
result = await server.input_text(f"text{char}here")
assert result.success is False
assert "clipboard_set" in result.error
async def test_rejects_semicolon_injection(self, server):
result = await server.input_text("hello; rm -rf /")
assert result.success is False
class TestClipboardSet:
async def test_cmd_clipboard(self, server):
server.run_shell_args.return_value = ok()
result = await server.clipboard_set("test text")
assert result.success is True
assert result.action == "clipboard_set"
async def test_cmd_clipboard_not_implemented_falls_back(self, server):
# First call: cmd clipboard returns "no shell command"
# Second call: am broadcast succeeds with result=-1
server.run_shell_args.side_effect = [
ok(stderr="No shell command implementation."),
ok(stdout="Broadcast completed: result=-1"),
]
result = await server.clipboard_set("test")
assert result.success is True
assert server.run_shell_args.call_count == 2
async def test_no_receiver_reports_failure(self, server):
server.run_shell_args.side_effect = [
ok(stderr="No shell command implementation."),
ok(stdout="Broadcast completed: result=0"), # No receiver
]
result = await server.clipboard_set("test")
assert result.success is False
assert "no broadcast receiver" in result.error.lower()
async def test_paste(self, server):
# First call: cmd clipboard set, second call: paste keyevent
server.run_shell_args.side_effect = [ok(), ok()]
result = await server.clipboard_set("text", paste=True)
assert result.success is True
assert result.pasted is True
# Verify KEYCODE_PASTE was sent
paste_call = server.run_shell_args.call_args_list[1]
assert "KEYCODE_PASTE" in paste_call[0][0]
async def test_text_preview_truncated(self, server):
server.run_shell_args.return_value = ok()
long_text = "x" * 200
result = await server.clipboard_set(long_text)
assert len(result.text) < 200
assert result.text.endswith("...")
class TestInputLongPress:
@pytest.mark.usefixtures("_dev_mode")
async def test_long_press(self, server):
server.run_shell_args.return_value = ok()
result = await server.input_long_press(100, 200, duration_ms=2000)
assert result.success is True
assert result.action == "long_press"
assert result.duration_ms == 2000
# Long press = swipe from same point to same point
args = server.run_shell_args.call_args[0][0]
assert args[2] == args[4] # x1 == x2
assert args[3] == args[5] # y1 == y2
@pytest.mark.usefixtures("_no_dev_mode")
async def test_requires_dev_mode(self, server):
result = await server.input_long_press(0, 0)
assert result.success is False
assert "developer mode" in result.error.lower()
class TestShellCommand:
@pytest.mark.usefixtures("_dev_mode")
async def test_executes(self, server):
server.run_shell.return_value = ok(stdout="output")
result = await server.shell_command("ls /sdcard")
assert result.success is True
assert result.stdout == "output"
assert result.command == "ls /sdcard"
@pytest.mark.usefixtures("_no_dev_mode")
async def test_requires_dev_mode(self, server):
result = await server.shell_command("ls")
assert result.success is False
assert "developer mode" in result.error.lower()