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

189 lines
10 KiB
Python

"""Local-only cross-machine capture/navigation checks; all desktop input mocked."""
import unittest
from unittest import mock
import numpy as np
from PIL import Image
import wechat_bot as module
from wechat_bot import WeChatBot, WindowCaptureError
from test_message_navigation import navigation_bot, narrow_surface, labels
def frame(width=400,height=300):
pixels=np.full((height,width,4),245,dtype=np.uint8)
pixels[:,:,3]=255
pixels[:,:58,:3]=(240,220,205)
pixels[70:92,8:48,:3]=(235,120,35)
pixels[100:130,80:160,:3]=25
return pixels
class WindowCaptureTests(unittest.TestCase):
def setUp(self):
self.rect=(100,200,500,500)
self.pixels=frame()
for patcher in (
mock.patch.object(module.win32gui,"GetWindowRect",return_value=self.rect),
mock.patch.object(module,"window_dpi_virtualization_factor",return_value=1),
):
patcher.start()
self.addCleanup(patcher.stop)
def test_valid_gpu_frame_uses_no_desktop_capture(self):
with mock.patch.object(module,"_capture_window_bitmap",return_value=self.pixels), mock.patch.object(module,"_capture_visible_window") as fallback:
result=module.capture_window_region(100,58,90,160,80)
np.testing.assert_array_equal(result,self.pixels[90:170,58:218])
fallback.assert_not_called()
def test_black_gpu_frame_uses_visible_fallback(self):
with mock.patch.object(module,"_capture_window_bitmap",return_value=np.zeros_like(self.pixels)), mock.patch.object(module,"_capture_visible_window",return_value=self.pixels) as fallback:
result=module.capture_window_region(100,58,90,160,80)
np.testing.assert_array_equal(result,self.pixels[90:170,58:218])
fallback.assert_called_once_with(100,self.rect)
def test_gpu_api_failure_uses_visible_fallback(self):
with mock.patch.object(module,"_capture_window_bitmap",side_effect=RuntimeError("failed")), mock.patch.object(module,"_capture_visible_window",return_value=self.pixels):
np.testing.assert_array_equal(module.capture_window_region(100,0,0,400,300),self.pixels)
def test_both_blank_sources_fail_instead_of_zero_unread(self):
with mock.patch.object(module,"_capture_window_bitmap",return_value=np.zeros_like(self.pixels)), mock.patch.object(module,"_capture_visible_window",return_value=np.zeros_like(self.pixels)):
with self.assertRaises(WindowCaptureError):
module.capture_window_region(100,0,0,400,300)
def test_dpi_bitmap_matches_physical_mouse_coordinates(self):
high=np.repeat(np.repeat(self.pixels,2,axis=0),2,axis=1)
high[:,:,3]=0 # Real GDI bitmaps frequently contain unused zero alpha.
with mock.patch.object(module,"window_dpi_virtualization_factor",return_value=2), mock.patch.object(module,"_capture_window_bitmap",return_value=high) as bitmap:
result=module.capture_window_region(100,0,0,400,300)
bitmap.assert_called_once_with(100,800,600)
self.assertEqual(result.shape,self.pixels[:,:,:3].shape)
self.assertLess(float(np.abs(result.astype(float)-self.pixels[:,:,:3]).mean()),2)
def test_window_move_invalidates_frame(self):
with mock.patch.object(module.win32gui,"GetWindowRect",side_effect=[self.rect,(200,200,600,500)]), mock.patch.object(module,"_capture_window_bitmap",return_value=self.pixels):
with self.assertRaises(WindowCaptureError):
module.capture_window_region(100,0,0,400,300)
def test_uniform_color_fails_but_dark_ui_is_valid(self):
for value in (0,32,240,255):
self.assertFalse(module.frame_has_content(np.full((400,600,3),value,dtype=np.uint8)))
solid=np.empty((400,600,3),dtype=np.uint8)
solid[:]=(240,220,205)
self.assertFalse(module.frame_has_content(solid))
dark=np.full((400,600,3),32,dtype=np.uint8)
dark[:,:60]=64
self.assertTrue(module.frame_has_content(dark))
def test_outside_region_is_rejected(self):
with mock.patch.object(module,"_capture_window_bitmap",return_value=self.pixels):
with self.assertRaises(WindowCaptureError):
module.capture_window_region(100,-10,0,100,100)
def test_foreground_and_occlusion_are_checked(self):
with mock.patch.object(module.win32gui,"GetForegroundWindow",return_value=100) as foreground, mock.patch.object(module.win32gui,"IsWindowVisible",return_value=True), mock.patch.object(module.win32gui,"IsIconic",return_value=False), mock.patch.object(module.win32gui,"WindowFromPoint",return_value=100) as hit, mock.patch.object(module.win32gui,"IsChild",return_value=False):
self.assertTrue(module._window_screen_capture_allowed(100,self.rect))
hit.return_value=200
self.assertFalse(module._window_screen_capture_allowed(100,self.rect))
hit.return_value=100
foreground.return_value=200
self.assertFalse(module._window_screen_capture_allowed(100,self.rect))
def test_visible_capture_uses_window_bbox_and_checks_after_capture(self):
with mock.patch.object(module,"_window_screen_capture_allowed",side_effect=[True,True]) as allowed, mock.patch("PIL.ImageGrab.grab",return_value=Image.fromarray(self.pixels[:,:,:3])) as grab:
result=module._capture_visible_window(100,self.rect)
self.assertEqual(result.shape,(300,400,3))
self.assertEqual(allowed.call_count,2)
grab.assert_called_once_with(bbox=self.rect,all_screens=True)
with mock.patch.object(module,"_window_screen_capture_allowed",side_effect=[True,False]), mock.patch("PIL.ImageGrab.grab",return_value=Image.fromarray(self.pixels[:,:,:3])):
with self.assertRaises(WindowCaptureError):
module._capture_visible_window(100,self.rect)
def test_failed_printwindow_releases_gdi_handles(self):
dc,memory,bitmap=mock.Mock(),mock.Mock(),mock.Mock()
dc.CreateCompatibleDC.return_value=memory
with mock.patch.object(module.win32gui,"GetWindowDC",return_value=12), mock.patch.object(module.win32ui,"CreateDCFromHandle",return_value=dc), mock.patch.object(module.win32ui,"CreateBitmap",return_value=bitmap), mock.patch.object(module.ctypes.windll.user32,"PrintWindow",return_value=0), mock.patch.object(module.win32gui,"DeleteObject") as delete, mock.patch.object(module.win32gui,"ReleaseDC") as release:
with self.assertRaises(WindowCaptureError):
module._capture_window_bitmap(100,400,300)
delete.assert_called_once_with(bitmap.GetHandle())
release.assert_called_once_with(100,12)
dc.DeleteDC.assert_called_once()
memory.DeleteDC.assert_called_once()
class OtherComputerNavigationTests(unittest.TestCase):
def test_capture_to_unread_click_across_scales_and_capture_backends(self):
from packaging_checks import check_cross_machine_capture
check_cross_machine_capture()
def test_gray_dark_sidebars_at_multiple_scales(self):
for scale in (1,1.25,1.5,2):
for color in (190,32):
with self.subTest(scale=scale,color=color):
bot=navigation_bot(scale)
surface=narrow_surface(scale)
surface[:,:int(58*scale),:]=color
width,confidence=module.infer_navigation_width(surface,scale)
self.assertAlmostEqual(width,58*scale,delta=2)
self.assertGreaterEqual(confidence,.85)
self.assertTrue(bot._message_nav_band(surface)[2])
def test_blue_workbench_is_not_messages_without_label(self):
bot=navigation_bot(1)
bot._strict_visual_actions=True
bot._name_reader_instance.read_layout.return_value=[]
surface=narrow_surface(1)
surface[65:105,2:58,:]=(238,126,36)
self.assertFalse(bot._message_nav_selected(surface))
def test_same_size_layout_change_forces_fresh_location(self):
bot=navigation_bot()
surface=narrow_surface()
original=bot._message_nav_band(surface)
moved=surface.copy()
moved[210:220,20:50,:]=55
bot._name_reader_instance.read_layout.return_value=labels(message_y=230)
self.assertNotEqual(bot._message_nav_band(moved),original)
self.assertEqual(bot._name_reader_instance.read_layout.call_count,2)
def test_failed_unread_lookup_retries(self):
bot=navigation_bot()
bot._locate_unread_group_band_by_text=mock.Mock(side_effect=[None,(400,440)])
surface=narrow_surface()
self.assertIsNone(bot._unread_group_band(surface))
self.assertEqual(bot._unread_group_band(surface),(400,440))
def test_failed_frame_never_triggers_model_or_mouse(self):
bot=WeChatBot.__new__(WeChatBot)
bot._drop_stale_review_flags=mock.Mock()
bot.wait_for_mouse_idle=mock.Mock(return_value=True)
bot._dismiss_owned_blocking_window=mock.Mock(return_value=False)
bot._ensure_visible=mock.Mock(return_value=True)
bot._security_gate_visible=mock.Mock(return_value=False)
bot._capture_full_window=mock.Mock(side_effect=WindowCaptureError("blank"))
bot._recover_capture_failure=mock.Mock()
bot._run_ai_page_guard=mock.Mock()
with mock.patch.object(module.pyautogui,"click") as click:
bot._poll_once()
bot._recover_capture_failure.assert_called_once_with("blank")
bot._run_ai_page_guard.assert_not_called()
click.assert_not_called()
def test_alive_empty_window_reselects_main_and_preserves_tasks(self):
bot=WeChatBot.__new__(WeChatBot)
bot.hwnd=100
bot._active_session_fp=b"old"
bot._pending_reply_sessions={"customer":{"stage":"queued"}}
bot._pending_scan_progress={"old":{}}
bot.report_progress=mock.Mock()
bot.connect=mock.Mock(return_value=True)
with mock.patch.object(module,"find_wx_hwnd",return_value=200), mock.patch.object(module,"window_looks_rendered",return_value=True):
bot._recover_capture_failure("blank")
bot.connect.assert_called_once_with(activate=False)
self.assertFalse(bot._window_ready)
self.assertIsNone(bot._active_session_fp)
self.assertEqual(bot._pending_scan_progress,{})
self.assertIn("customer",bot._pending_reply_sessions)
if __name__ == "__main__":
unittest.main()