import importlib.util from pathlib import Path import unittest import sys sys.path.insert(0, str(Path(__file__).parents[1] / "vm")) import tempfile import json from unittest.mock import patch, Mock from types import SimpleNamespace from contextlib import ExitStack import io spec = importlib.util.spec_from_file_location("desktop", Path(__file__).parents[1] / "vm/desktop.py") desktop = importlib.util.module_from_spec(spec) spec.loader.exec_module(desktop) class ValidationTests(unittest.TestCase): def test_settling_delay_precedes_final_capture_only_after_actions(self): for actions in ([], [{"type": "click", "x": 500, "y": 500}]): events = [] image = Mock(size=(1280, 800), width=1280, height=800) def grab(**kwargs): events.append("capture") return image with tempfile.TemporaryDirectory() as directory, ExitStack() as stack: state = Path(directory) (state / "session.json").write_text('{"DISPLAY": ":0"}') stack.enter_context(patch.object(desktop, "STATE", state)) stack.enter_context(patch.object(sys, "argv", ["desktop.py"])) stack.enter_context(patch.object(sys, "stdin", io.StringIO(json.dumps({"actions": actions, "coordinateSpace": "normalized_1000"})))) stack.enter_context(patch.object(sys, "stdout", io.StringIO())) stack.enter_context(patch.dict(sys.modules, {"PIL": SimpleNamespace(ImageGrab=SimpleNamespace(grab=grab))})) stack.enter_context(patch("time.sleep", side_effect=lambda seconds: events.append(seconds))) command = stack.enter_context(patch.object(desktop, "run", side_effect=lambda *args: events.append("action"))) stack.enter_context(patch.object(desktop.accessibility, "collect", return_value={"targets": [], "warning": None})) stack.enter_context(patch.object(desktop.accessibility, "annotate", return_value=image)) desktop.main() if actions: self.assertEqual(events, ["capture", "action", 0.5, "capture"]) command.assert_called_once_with("xdotool", "mousemove", "--sync", "640", "500", "click", "1") else: self.assertEqual(events, ["capture", "capture"]) def test_session_save(self): with tempfile.TemporaryDirectory() as directory: state = Path(directory) with patch.object(desktop, "STATE", state): desktop.save_session({"DISPLAY": ":0"}) desktop.save_session({"DISPLAY": ":1"}) path = state / "session.json" self.assertEqual(json.loads(path.read_text()), {"DISPLAY": ":1"}) self.assertEqual(path.stat().st_mode & 0o777, 0o600) self.assertEqual(len(list(state.iterdir())), 1) def test_autostart(self): with tempfile.TemporaryDirectory() as directory: with patch.object(Path, "home", return_value=Path(directory)): desktop.install_autostart() desktop.install_autostart() entries = list((Path(directory) / ".config/autostart").iterdir()) self.assertEqual(len(entries), 1) self.assertIn("--register-session\n", entries[0].read_text()) self.assertIn("OnlyShowIn=XFCE;", entries[0].read_text()) def test_normalized_coordinates(self): # 1280x800: width > 1000 (normalized 0..1000), height <= 1000 (pixels 0..799) self.assertEqual(desktop.click_pixels({"x": 0, "y": 0}, 1280, 800, "normalized_1000"), (0, 0)) self.assertEqual(desktop.click_pixels({"x": 500, "y": 400}, 1280, 800, "normalized_1000"), (640, 400)) self.assertEqual(desktop.click_pixels({"x": 1000, "y": 799}, 1280, 800, "normalized_1000"), (1279, 799)) # 1920x1080: both > 1000 (both normalized 0..1000) self.assertEqual(desktop.click_pixels({"x": 500, "y": 500}, 1920, 1080, "normalized_1000"), (960, 540)) # 800x600: both <= 1000 (both pixels) self.assertEqual(desktop.click_pixels({"x": 400, "y": 300}, 800, 600, "normalized_1000"), (400, 300)) # 1x1: both <= 1000 self.assertEqual(desktop.click_pixels({"x": 0, "y": 0}, 1, 1, "normalized_1000"), (0, 0)) # pixels mode self.assertEqual(desktop.click_pixels({"x": 480, "y": 425}, 1280, 800, "pixels"), (480, 425)) for value in (-1, 1001, 0.5, True): with self.assertRaises(ValueError): desktop.click_pixels({"x": value, "y": 0}, 1280, 800, "normalized_1000") def test_invalid_coordinate_space(self): with self.assertRaises(ValueError): desktop.validate({"actions": [], "coordinateSpace": "guess"}, 1280, 800) def test_capture(self): self.assertEqual(desktop.validate({"actions": []}, 100, 100), []) def test_bounds(self): for x in (-1, 100, True, "2"): with self.assertRaises(ValueError): desktop.validate({"actions": [{"type": "click", "x": x, "y": 0}]}, 100, 100) def test_invalid_keys(self): with self.assertRaises(ValueError): desktop.validate({"actions": [{"type": "keys", "keys": ["--repeat"]}]}, 100, 100) def test_wait_budget(self): with self.assertRaises(ValueError): desktop.validate({"actions": [{"type": "wait", "milliseconds": 3000}] * 2}, 100, 100) if __name__ == "__main__": unittest.main()