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