342 lines
12 KiB
Python
342 lines
12 KiB
Python
#!/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()
|