File size: 13,263 Bytes
1f21206 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 | #!/usr/bin/env python3
"""Cross-platform tests for mac_helper.py and win_helper.py.
Tests the platform-independent parts (JSON protocol, key mapping, capture logic)
without requiring platform-specific dependencies. Can run on any OS with pytest.
Usage:
python -m pytest runtime/test_helpers.py -v
# or simply:
python runtime/test_helpers.py
"""
from __future__ import annotations
import json
import subprocess
import sys
import unittest
from pathlib import Path
from unittest.mock import patch, MagicMock
# Determine which helper to test based on current platform
IS_WINDOWS = sys.platform == "win32"
IS_MACOS = sys.platform == "darwin"
RUNTIME_DIR = Path(__file__).parent
MAC_HELPER = RUNTIME_DIR / "mac_helper.py"
WIN_HELPER = RUNTIME_DIR / "win_helper.py"
class TestKeyMap(unittest.TestCase):
"""Test the KEY_MAP and normalize_key function — platform-independent logic."""
def _load_key_map(self, helper_path: Path) -> dict[str, str]:
"""Extract KEY_MAP from a helper by importing it with mocked deps."""
# Read the file and extract just the KEY_MAP dict
source = helper_path.read_text()
# Find KEY_MAP definition
start = source.index("KEY_MAP = {")
# Find the matching closing brace
depth = 0
for i, ch in enumerate(source[start:], start):
if ch == "{":
depth += 1
elif ch == "}":
depth -= 1
if depth == 0:
end = i + 1
break
key_map_source = source[start:end]
ns: dict = {}
exec(key_map_source, ns)
return ns["KEY_MAP"]
def test_mac_key_map_exists(self):
if not MAC_HELPER.exists():
self.skipTest("mac_helper.py not found")
km = self._load_key_map(MAC_HELPER)
self.assertIn("cmd", km)
self.assertIn("ctrl", km)
self.assertEqual(km["cmd"], "command")
self.assertEqual(km["alt"], "option")
def test_win_key_map_exists(self):
if not WIN_HELPER.exists():
self.skipTest("win_helper.py not found")
km = self._load_key_map(WIN_HELPER)
self.assertIn("cmd", km)
self.assertIn("ctrl", km)
# Windows maps cmd/command/meta to 'win' key
self.assertEqual(km["cmd"], "win")
self.assertEqual(km["command"], "win")
self.assertEqual(km["meta"], "win")
# Windows maps alt/option to 'alt'
self.assertEqual(km["alt"], "alt")
self.assertEqual(km["option"], "alt")
def test_common_keys_present_in_both(self):
"""Both helpers must have the same set of key names."""
if not MAC_HELPER.exists() or not WIN_HELPER.exists():
self.skipTest("Both helpers required")
mac_km = self._load_key_map(MAC_HELPER)
win_km = self._load_key_map(WIN_HELPER)
# All keys in mac should be in win and vice versa
self.assertEqual(set(mac_km.keys()), set(win_km.keys()),
"KEY_MAP keys must be identical across platforms")
def test_all_alphabet_keys(self):
"""All a-z keys should map to themselves."""
for helper in [MAC_HELPER, WIN_HELPER]:
if not helper.exists():
continue
km = self._load_key_map(helper)
for char in "abcdefghijklmnopqrstuvwxyz":
self.assertEqual(km[char], char, f"{helper.name}: {char} should map to itself")
def test_all_digit_keys(self):
"""All 0-9 keys should map to themselves."""
for helper in [MAC_HELPER, WIN_HELPER]:
if not helper.exists():
continue
km = self._load_key_map(helper)
for digit in "0123456789":
self.assertEqual(km[digit], digit, f"{helper.name}: {digit} should map to itself")
def test_function_keys(self):
"""F1-F12 should map to themselves."""
for helper in [MAC_HELPER, WIN_HELPER]:
if not helper.exists():
continue
km = self._load_key_map(helper)
for i in range(1, 13):
key = f"f{i}"
self.assertEqual(km[key], key, f"{helper.name}: {key} should map to itself")
class TestJSONProtocol(unittest.TestCase):
"""Test that both helpers follow the same JSON command protocol."""
def _get_helper(self) -> Path:
"""Get the appropriate helper for the current platform."""
if IS_WINDOWS and WIN_HELPER.exists():
return WIN_HELPER
if IS_MACOS and MAC_HELPER.exists():
return MAC_HELPER
return MAC_HELPER if MAC_HELPER.exists() else WIN_HELPER
def _parse_main_commands(self, helper_path: Path) -> list[str]:
"""Extract all command names from the main() dispatcher."""
source = helper_path.read_text()
commands = []
for line in source.splitlines():
stripped = line.strip()
if stripped.startswith('if command == "'):
cmd = stripped.split('"')[1]
commands.append(cmd)
return commands
def test_both_helpers_same_commands(self):
"""Both helpers must support the exact same set of commands."""
if not MAC_HELPER.exists() or not WIN_HELPER.exists():
self.skipTest("Both helpers required")
mac_cmds = set(self._parse_main_commands(MAC_HELPER))
win_cmds = set(self._parse_main_commands(WIN_HELPER))
self.assertEqual(mac_cmds, win_cmds,
f"Command sets differ.\nOnly in mac: {mac_cmds - win_cmds}\nOnly in win: {win_cmds - mac_cmds}")
def test_expected_commands_exist(self):
"""Core commands should be present in each helper."""
expected = {
"check_permissions", "list_displays", "get_display_size",
"screenshot", "resolve_prepare_capture", "zoom",
"prepare_for_action", "preview_hide_set", "find_window_displays",
"key", "hold_key", "type", "click", "drag",
"move_mouse", "scroll", "mouse_down", "mouse_up",
"cursor_position", "frontmost_app", "app_under_point",
"list_installed_apps", "list_running_apps", "open_app",
"read_clipboard", "write_clipboard", "paste_clipboard",
}
for helper in [MAC_HELPER, WIN_HELPER]:
if not helper.exists():
continue
cmds = set(self._parse_main_commands(helper))
missing = expected - cmds
self.assertFalse(missing,
f"{helper.name} missing commands: {missing}")
def test_unknown_command_returns_error(self):
"""Running a non-existent command should return a JSON error."""
helper = self._get_helper()
if not helper.exists():
self.skipTest("No helper found")
# On macOS without venv, mac_helper.py may fail at import (AppKit);
# on Windows without venv, win_helper.py may fail at import (win32gui).
# Only test if the helper can actually import.
check = subprocess.run(
[sys.executable, "-c", f"import importlib.util; "
f"spec = importlib.util.spec_from_file_location('h', '{helper}')"],
capture_output=True, text=True
)
result = subprocess.run(
[sys.executable, str(helper), "nonexistent_command_xyz"],
capture_output=True, text=True
)
if result.returncode == 1 and not result.stdout.strip():
# Import failed — platform deps missing, skip this test
self.skipTest(f"Cannot run {helper.name} on this platform (missing deps)")
# Should exit with code 2
self.assertEqual(result.returncode, 2)
parsed = json.loads(result.stdout.strip())
self.assertFalse(parsed["ok"])
self.assertEqual(parsed["error"]["code"], "bad_command")
class TestHelperOutputFormat(unittest.TestCase):
"""Test the JSON output helpers are consistent."""
def test_json_output_function_exists(self):
"""Both helpers should define json_output and error_output."""
for helper in [MAC_HELPER, WIN_HELPER]:
if not helper.exists():
continue
source = helper.read_text()
self.assertIn("def json_output(", source,
f"{helper.name} missing json_output function")
self.assertIn("def error_output(", source,
f"{helper.name} missing error_output function")
def test_main_entry_point(self):
"""Both helpers should have the standard main entry point."""
for helper in [MAC_HELPER, WIN_HELPER]:
if not helper.exists():
continue
source = helper.read_text()
self.assertIn('if __name__ == "__main__":', source,
f"{helper.name} missing __main__ guard")
self.assertIn("def main()", source,
f"{helper.name} missing main() function")
class TestWinHelperPermissions(unittest.TestCase):
"""Windows-specific: permissions should always return True."""
def test_check_permissions_always_granted(self):
"""On Windows, permissions are not needed — should always be True."""
if not WIN_HELPER.exists():
self.skipTest("win_helper.py not found")
# Extract and exec just the check_permissions function
source = WIN_HELPER.read_text()
# Find the function
self.assertIn("def check_permissions()", source)
# The function should return both as True
# We can verify by reading the source
start = source.index("def check_permissions()")
# Find next def or end
rest = source[start:]
lines = rest.split("\n")
func_lines = [lines[0]]
for line in lines[1:]:
if line and not line[0].isspace() and not line.startswith("#"):
break
func_lines.append(line)
func_source = "\n".join(func_lines)
self.assertIn('"accessibility": True', func_source)
self.assertIn('"screenRecording": True', func_source)
class TestMacHelperPermissions(unittest.TestCase):
"""macOS helper permission detection should use the official trust API."""
def test_check_permissions_uses_ax_api_instead_of_system_events(self):
if not MAC_HELPER.exists():
self.skipTest("mac_helper.py not found")
source = MAC_HELPER.read_text()
self.assertIn("def detect_accessibility_permission()", source)
self.assertIn("AXIsProcessTrusted", source)
start = source.index("def check_permissions()")
rest = source[start:]
lines = rest.split("\n")
func_lines = [lines[0]]
for line in lines[1:]:
if line and not line[0].isspace() and not line.startswith("#"):
break
func_lines.append(line)
func_source = "\n".join(func_lines)
self.assertIn("detect_accessibility_permission()", func_source)
self.assertNotIn('tell application "System Events"', func_source)
def test_clipboard_shortcuts_use_osascript_path(self):
if not MAC_HELPER.exists():
self.skipTest("mac_helper.py not found")
source = MAC_HELPER.read_text()
self.assertIn("def paste_clipboard()", source)
self.assertIn('send_keystroke_via_osascript("v", ["command"])', source)
self.assertIn('if parts == ["command", "v"]:', source)
self.assertIn('elif parts == ["command", "a"]:', source)
class TestCrossPlatformFunctions(unittest.TestCase):
"""Test functions that are identical between both helpers."""
def _get_function_body(self, helper_path: Path, func_name: str) -> str:
"""Extract a function's body (code lines only, no comments/blanks)."""
source = helper_path.read_text()
marker = f"def {func_name}("
if marker not in source:
return ""
start = source.index(marker)
rest = source[start:]
lines = rest.split("\n")
func_lines = [lines[0]]
for line in lines[1:]:
# Stop at next top-level def/class or non-indented non-empty line
stripped = line.strip()
if line and not line[0].isspace() and stripped and not stripped.startswith("#"):
break
# Skip comments and blank lines for comparison
if stripped.startswith("#") or not stripped:
continue
func_lines.append(line)
return " ".join(" ".join(func_lines).split())
def test_input_functions_identical(self):
"""Input action functions (click, scroll, etc.) should be identical."""
if not MAC_HELPER.exists() or not WIN_HELPER.exists():
self.skipTest("Both helpers required")
for func in ["click", "scroll", "hold_keys", "type_text"]:
mac_src = self._get_function_body(MAC_HELPER, func)
win_src = self._get_function_body(WIN_HELPER, func)
self.assertEqual(mac_src, win_src,
f"{func} should be identical across platforms")
if __name__ == "__main__":
unittest.main()
|