"""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()