Test infrastructure with conftest fixtures mocking run_shell_args/run_adb for device-free testing across all 8 mixins. Fixed: UI parser regex couldn't match hyphenated XML attributes (content-desc, resource-id). Notification parser captured trailing parenthesis in package names.
237 lines
8.9 KiB
Python
237 lines
8.9 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["from"] == {"x": 0, "y": 100}
|
|
assert result["to"] == {"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()
|