189 lines
10 KiB
Python
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()
|