138 lines
6.5 KiB
Python
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()
|