Files
kefu/wechat_rpa/test_message_navigation.py
T
2026-09-21 10:34:06 +08:00

138 lines
6.5 KiB
Python

"""Synthetic WeCom sidebar regressions; real desktop input is always mocked."""
import unittest
from unittest import mock
import numpy as np
from wechat_bot import WeChatBot
def narrow_surface(scale=2.0):
image=np.full((int(740*scale),int(600*scale),3),245,dtype=np.uint8)
image[:,:int(58*scale),:]=(250,232,215)
return image
def labels(scale=2.0, message_y=152):
def item(text,x1,y1,x2,y2):
return {"text":text,"score":0.99,"x1":int(x1*scale),"y1":int(y1*scale),"x2":int(x2*scale),"y2":int(y2*scale)}
return [item("智能助理",7,96,50,108),item("消息",17,message_y,39,message_y+12)]
def navigation_bot(scale=2.0):
bot=WeChatBot.__new__(WeChatBot)
bot.scale=scale; bot.hwnd=100
reader=mock.Mock()
reader.available=True; reader.read_layout.return_value=labels(scale)
bot._name_reader_instance=reader
return bot
class MessageNavigationTest(unittest.TestCase):
def test_stacked_labels_locate_messages_at_multiple_dpi_scales(self):
for scale in (1.0,1.25,1.5,2.0,2.5,3.0):
with self.subTest(scale=scale):
bot=navigation_bot(scale)
y1,y2,located=bot._message_nav_band(narrow_surface(scale))
self.assertTrue(located)
self.assertTrue(120*scale < (y1+y2)/2 < 168*scale)
crop=bot._name_reader_instance.read_layout.call_args.args[0]
self.assertEqual(crop.shape[1],int(58*scale))
bot._name_reader_instance.read_raw.assert_not_called()
def test_message_text_must_be_exact_and_unique(self):
for value in ("智能助理", "新消息通知", "消息设置", "客户消息", ""):
bot=navigation_bot()
bot._name_reader_instance.read_layout.return_value=[dict(labels()[1],text=value)]
self.assertFalse(bot._message_nav_band(narrow_surface())[2])
bot=navigation_bot()
bot._name_reader_instance.read_layout.return_value=[labels()[1],dict(labels()[1],y1=500,y2=524)]
self.assertFalse(bot._message_nav_band(narrow_surface())[2])
def test_badge_digits_are_allowed_on_message_label(self):
for label in ("消息1", "消息99+"):
bot=navigation_bot()
bot._name_reader_instance.read_layout.return_value=[dict(labels()[1],text=label)]
self.assertTrue(bot._message_nav_band(narrow_surface())[2])
def test_untrusted_boxes_or_low_confidence_cannot_select_a_target(self):
for replacement in ({"score":0.3},{"x2":200},{"x1":-1},{"y1":-1},{"y2":10000}):
bot=navigation_bot()
bot._name_reader_instance.read_layout.return_value=[dict(labels()[1],**replacement)]
self.assertFalse(bot._message_nav_band(narrow_surface())[2])
def test_temporary_ocr_failure_is_not_cached(self):
bot=navigation_bot()
bot._name_reader_instance.read_layout.side_effect=[[],labels()]
image=narrow_surface()
self.assertFalse(bot._message_nav_band(image)[2])
self.assertTrue(bot._message_nav_band(image)[2])
self.assertEqual(bot._name_reader_instance.read_layout.call_count,2)
def test_successful_position_is_cached_for_passive_scans(self):
bot=navigation_bot(); image=narrow_surface()
self.assertEqual(bot._message_nav_band(image),bot._message_nav_band(image))
bot._name_reader_instance.read_layout.assert_called_once()
def test_selected_assistant_does_not_count_as_messages(self):
bot=navigation_bot(); image=narrow_surface()
image[130:210,4:112,:]=(238,126,36)
self.assertFalse(bot._message_nav_selected(image))
y1,y2,_=bot._message_nav_band(image)
image[130:210,4:112,:]=(250,232,215)
image[y1:y2,4:112,:]=(238,126,36)
self.assertTrue(bot._message_nav_selected(image))
def prepare_click(self,bot,image):
bot.wait_for_mouse_idle=mock.Mock(return_value=True)
bot._security_gate_visible=mock.Mock(return_value=False)
bot._begin_bot_mouse=mock.Mock(); bot._end_bot_mouse=mock.Mock()
bot._capture_full_window=mock.Mock(return_value=image)
bot._refresh_message_geometry=mock.Mock()
for patcher in (
mock.patch("wechat_bot.safe_set_foreground",return_value=True),
mock.patch("wechat_bot.win32gui.GetForegroundWindow",return_value=100),
mock.patch("wechat_bot.win32gui.GetWindowRect",return_value=(400,300,1600,1780)),
mock.patch("wechat_bot.time.sleep"),
):
patcher.start(); self.addCleanup(patcher.stop)
def test_click_refreshes_stale_position_after_navigation_reorder(self):
bot=navigation_bot(); image=narrow_surface()
self.prepare_click(bot,image)
bot._msg_nav_band_cache=((image.shape[0],image.shape[1]),(130,210,True))
bot._message_nav_selected=mock.Mock(return_value=True)
with mock.patch("wechat_bot.pyautogui.click") as click:
self.assertTrue(bot._open_messages_page("test"))
x,y=click.call_args.args
self.assertTrue(400 < x < 516)
self.assertTrue(540 < y < 636)
click.assert_called_once()
def test_no_click_when_message_label_cannot_be_confirmed(self):
for failure in ([], [labels()[0]], RuntimeError("OCR unavailable")):
bot=navigation_bot(); image=narrow_surface()
self.prepare_click(bot,image)
if isinstance(failure,Exception):
bot._name_reader_instance.read_layout.side_effect=failure
else:
bot._name_reader_instance.read_layout.return_value=failure
with mock.patch("wechat_bot.pyautogui.click") as click:
self.assertFalse(bot._open_messages_page("test"))
click.assert_not_called()
def test_capture_failure_never_clicks_a_fallback_coordinate(self):
bot=navigation_bot(); self.prepare_click(bot,narrow_surface())
bot._capture_full_window.side_effect=RuntimeError("capture failed")
with mock.patch("wechat_bot.pyautogui.click") as click:
self.assertFalse(bot._open_messages_page("test"))
click.assert_not_called()
def test_foreground_failure_prevents_input(self):
bot=navigation_bot(); self.prepare_click(bot,narrow_surface())
with mock.patch("wechat_bot.safe_set_foreground",return_value=False), mock.patch("wechat_bot.time.monotonic",side_effect=[0,2]), mock.patch("wechat_bot.pyautogui.click") as click:
self.assertFalse(bot._open_messages_page("test"))
click.assert_not_called()
if __name__ == "__main__":
unittest.main()