Files
JWeBot/tests/test_wechat_adaptive_gui_bridge.py

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