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