#!/usr/bin/env python3 import importlib.util import sys import unittest import tempfile from pathlib import Path from unittest import mock from PIL import Image PROJECT_ROOT = Path(__file__).resolve().parents[1] MODULE_PATH = PROJECT_ROOT / "wechat_adaptive_gui_bridge.py" SPEC = importlib.util.spec_from_file_location("wechat_adaptive_gui_bridge", MODULE_PATH) MODULE = importlib.util.module_from_spec(SPEC) assert SPEC.loader is not None sys.modules[SPEC.name] = MODULE SPEC.loader.exec_module(MODULE) class CoordinateTests(unittest.TestCase): def test_baseline_coordinates_are_unchanged(self): self.assertEqual( MODULE.scaled_point( MODULE.BASE_DRAFT_POINT, width=MODULE.BASE_WIDTH, height=MODULE.BASE_HEIGHT, ), MODULE.BASE_DRAFT_POINT, ) def test_current_vnc_coordinates_are_scaled(self): self.assertEqual( MODULE.scaled_point( MODULE.BASE_DRAFT_POINT, width=1470, height=694, ), (450, 625), ) self.assertEqual( MODULE.scaled_point( MODULE.BASE_SEND_POINT, width=1470, height=694, ), (1419, 637), ) self.assertEqual( MODULE.scaled_box( MODULE.BASE_INPUT_BOX, width=1470, height=694, ), (306, 582, 1469, 693), ) self.assertEqual( MODULE.scaled_region( MODULE.BASE_INPUT_TEXT_ROI, width=1470, height=694, ), (327, 595, 653, 60), ) self.assertEqual(MODULE.INPUT_TEXT_ROI_NAME, "input_text_core_v2") class VncKeyboardTests(unittest.TestCase): def test_vnc_key_event_has_the_expected_wire_format(self): connection = mock.Mock() MODULE.vnc_key_event(connection, down=True, keysym=ord("v")) connection.sendall.assert_called_once_with( MODULE.struct.pack(">BBHI", 4, 1, 0, ord("v")) ) def test_x11_clipboard_rejects_a_failed_owner(self): process = mock.Mock() process.returncode = 1 with mock.patch.object( MODULE.subprocess, "Popen", return_value=process, ): with self.assertRaisesRegex(RuntimeError, "clipboard operation failed"): MODULE.set_x_clipboard("测试") def test_vnc_pointer_event_has_the_expected_wire_format(self): connection = mock.Mock() MODULE.vnc_pointer_event(connection, buttons=1, x=450, y=652) connection.sendall.assert_called_once_with( MODULE.struct.pack(">BBHH", 5, 1, 450, 652) ) def test_vnc_select_all_and_clear_uses_vnc_key_events(self): connection = mock.Mock() MODULE.vnc_select_all_and_clear(connection) payloads = [call.args[0] for call in connection.sendall.call_args_list] self.assertEqual( payloads, [ MODULE.struct.pack(">BBHI", 4, 1, 0, 0xFFE3), MODULE.struct.pack(">BBHI", 4, 1, 0, ord("a")), MODULE.struct.pack(">BBHI", 4, 0, 0, ord("a")), MODULE.struct.pack(">BBHI", 4, 0, 0, 0xFFE3), MODULE.struct.pack(">BBHI", 4, 1, 0, 0xFF08), MODULE.struct.pack(">BBHI", 4, 0, 0, 0xFF08), ], ) def test_vnc_return_key_uses_the_standard_enter_keysym(self): connection = mock.Mock() MODULE.vnc_key_press(connection, 0xFF0D) self.assertEqual( [call.args[0] for call in connection.sendall.call_args_list], [ MODULE.struct.pack(">BBHI", 4, 1, 0, 0xFF0D), MODULE.struct.pack(">BBHI", 4, 0, 0, 0xFF0D), ], ) class InputVerificationTests(unittest.TestCase): def test_changed_pixels_ignores_a_small_cursor_change(self): before = Image.new("RGB", (20, 20), "white") after = before.copy() after.putpixel((1, 1), (0, 0, 0)) self.assertEqual(MODULE.changed_pixels(before, after), 1) def test_changed_pixels_detects_pasted_text_region(self): before = Image.new("RGB", (20, 20), "white") after = before.copy() for x in range(10): for y in range(10): after.putpixel((x, y), (0, 0, 0)) self.assertGreaterEqual( MODULE.changed_pixels(before, after), MODULE.MIN_DRAFT_CHANGED_PIXELS, ) def test_changed_pixels_rejects_an_unchanged_send_region(self): image = Image.new("RGB", (20, 20), "white") self.assertLess( MODULE.changed_pixels(image, image.copy()), MODULE.MIN_DRAFT_CHANGED_PIXELS, ) def test_ink_pixels_detects_text_and_blank_roi(self): blank = Image.new("RGB", (20, 20), "white") text = blank.copy() for x in range(8): for y in range(8): text.putpixel((x, y), (0, 0, 0)) self.assertLessEqual(MODULE.ink_pixels(blank), MODULE.MAX_EMPTY_ROI_INK_PIXELS) self.assertGreater(MODULE.ink_pixels(text), MODULE.MAX_EMPTY_ROI_INK_PIXELS) def test_wait_for_input_empty_reports_poll_count(self): dirty = Image.new("RGB", (20, 20), "white") for x in range(8): for y in range(8): dirty.putpixel((x, y), (0, 0, 0)) blank = Image.new("RGB", (20, 20), "white") monotonic_values = iter((1.0, 1.0, 1.05, 1.05)) with mock.patch.object( MODULE, "input_image", side_effect=[dirty, blank], ), mock.patch.object( MODULE.time, "monotonic", side_effect=lambda: next(monotonic_values), ), mock.patch.object(MODULE.time, "sleep"): _, ink, polls, elapsed_ms = MODULE.wait_for_input_empty( "window", width=1470, height=694, timeout_seconds=MODULE.CLEAR_CONFIRM_TIMEOUT_SECONDS, ) self.assertLessEqual(ink, MODULE.MAX_EMPTY_ROI_INK_PIXELS) self.assertEqual(polls, 2) self.assertEqual(elapsed_ms, 50.0) def test_wait_for_input_change_reports_poll_count_and_duration(self): before = Image.new("RGB", (20, 20), "white") after = before.copy() for x in range(10): for y in range(10): after.putpixel((x, y), (0, 0, 0)) monotonic_values = iter((1.0, 1.0, 1.05)) with mock.patch.object( MODULE, "input_image", return_value=after, ), mock.patch.object( MODULE, "changed_pixels", return_value=MODULE.MIN_SEND_CHANGED_PIXELS, ), mock.patch.object( MODULE.time, "monotonic", side_effect=lambda: next(monotonic_values), ): _, changed, polls, elapsed_ms = MODULE.wait_for_input_change( "window", before, width=1470, height=694, threshold=MODULE.MIN_SEND_CHANGED_PIXELS, timeout_seconds=MODULE.SEND_CONFIRM_TIMEOUT_SECONDS, ) self.assertEqual(changed, MODULE.MIN_SEND_CHANGED_PIXELS) self.assertEqual(polls, 1) self.assertEqual(elapsed_ms, 50.0) class SendTests(unittest.TestCase): def setUp(self): self.temporary = tempfile.TemporaryDirectory() self.pending_text = Path(self.temporary.name) / "pending.txt" self.pending_token = Path(self.temporary.name) / "pending.token" self.pending_text.write_text("reply", encoding="utf-8") self.pending_token.write_text("token\n", encoding="utf-8") self.patches = [ mock.patch.object(MODULE, "PENDING_TEXT", self.pending_text), mock.patch.object(MODULE, "PENDING_TOKEN", self.pending_token), mock.patch.object(MODULE, "window_id", return_value="window"), mock.patch.object(MODULE, "geometry", return_value=(1470, 694)), mock.patch.object(MODULE, "position", return_value=(0, 0)), mock.patch.object(MODULE.time, "sleep"), ] for patch in self.patches: patch.start() def tearDown(self): for patch in reversed(self.patches): patch.stop() self.temporary.cleanup() def test_send_presses_return_first_after_roi_flag(self): connection = mock.Mock() before = Image.new("RGB", (20, 20), "white") after = before.copy() with mock.patch.object( MODULE, "vnc_focus_input", return_value=connection, ) as focus, mock.patch.object( MODULE, "input_image", return_value=before, ), mock.patch.object( MODULE, "wait_for_input_change", return_value=(after, MODULE.MIN_SEND_CHANGED_PIXELS, 1, 50.0), ) as wait: MODULE.send("token") focus.assert_called_once_with("window", (450, 625), origin=(0, 0)) connection.sendall.assert_any_call( MODULE.struct.pack(">BBHI", 4, 1, 0, 0xFF0D) ) self.assertEqual(wait.call_args.kwargs["box"], MODULE.BASE_INPUT_TEXT_ROI) self.assertFalse(self.pending_text.exists()) self.assertFalse(self.pending_token.exists()) def test_send_uses_button_when_return_is_not_confirmed(self): first_connection = mock.Mock() second_connection = mock.Mock() before = Image.new("RGB", (20, 20), "white") after_return = before.copy() after_click = before.copy() with mock.patch.object( MODULE, "vnc_focus_input", side_effect=[first_connection, second_connection], ) as focus, mock.patch.object( MODULE, "input_image", return_value=before, ), mock.patch.object( MODULE, "wait_for_input_change", side_effect=[ (after_click, 0, 18, 900.0), (after_return, MODULE.MIN_SEND_CHANGED_PIXELS, 1, 50.0), ], ): MODULE.send("token") self.assertEqual( [call.args for call in focus.call_args_list], [("window", (450, 625)), ("window", (1419, 637))], ) first_connection.sendall.assert_any_call( MODULE.struct.pack(">BBHI", 4, 1, 0, 0xFF0D) ) self.assertFalse(self.pending_text.exists()) self.assertFalse(self.pending_token.exists()) def test_deliver_text_confirms_clear_before_paste(self): connection = mock.Mock() blank = Image.new("RGB", (20, 20), "white") after_paste = blank.copy() after_send = blank.copy() with mock.patch.object( MODULE, "vnc_focus_input", return_value=connection, ), mock.patch.object( MODULE, "wait_for_input_empty", return_value=(blank, 0, 1, 50.0), ) as empty, mock.patch.object( MODULE, "vnc_paste", ) as paste, mock.patch.object( MODULE, "input_image", return_value=after_paste, ), mock.patch.object( MODULE, "wait_for_input_change", side_effect=[ (after_paste, MODULE.MIN_DRAFT_CHANGED_PIXELS, 1, 50.0), (after_send, MODULE.MIN_SEND_CHANGED_PIXELS, 1, 50.0), ], ) as wait: payload = MODULE.deliver_text("reply") self.assertEqual(payload["state"], "sent") empty.assert_called_once() paste.assert_called_once_with(connection, "reply") self.assertEqual(len(wait.call_args_list), 2) connection.sendall.assert_any_call( MODULE.struct.pack(">BBHI", 4, 1, 0, 0xFF0D) ) if __name__ == "__main__": unittest.main()