# -*- coding: utf-8 -*- """Qt 悬浮监听条的布局与交互回归测试。""" import json import os import tempfile from pathlib import Path from types import SimpleNamespace from unittest import TestCase, main, mock, skipUnless os.environ.setdefault("QT_QPA_PLATFORM", "offscreen") try: from PySide6.QtCore import QPoint, Qt from PySide6.QtGui import QFontMetrics from PySide6.QtWidgets import ( QApplication, QMainWindow, QMessageBox, QStackedWidget, QVBoxLayout, QWidget, ) except ImportError: QApplication = None else: from wechat_gui_qt import APP_QSS, CapsuleWindow, MainWindow, QueuePage @skipUnless(QApplication is not None, "当前测试环境未安装 PySide6") class CapsuleWindowTest(TestCase): @classmethod def setUpClass(cls): cls.app = QApplication.instance() or QApplication([]) cls.app.setStyleSheet(APP_QSS) def setUp(self): self.capsule = CapsuleWindow() self.capsule.show() self.app.processEvents() def tearDown(self): self.capsule.close() self.app.processEvents() def test_compact_layout_has_no_horizontal_overlap(self): self.assertEqual((self.capsule.width(), self.capsule.height()), (436, 112)) self.assertEqual((self.capsule.panel.width(), self.capsule.panel.height()), (420, 94)) widgets = ( self.capsule.status_group, self.capsule.divider, self.capsule.stop_button, self.capsule.expand_button, ) bounds = [] for widget in widgets: origin = widget.mapTo(self.capsule.panel, QPoint(0, 0)) bounds.append((origin.x(), origin.x() + widget.width() - 1)) for previous, current in zip(bounds, bounds[1:]): self.assertLess(previous[1], current[0]) # 当前进程独占底部整行,不再被压在计时器下面的 150px 小区域里。 self.assertGreaterEqual(self.capsule.progress.width(), 340) self.assertLess( self.capsule.status_group.mapTo(self.capsule.panel, QPoint(0, 0)).y() + self.capsule.status_group.height(), self.capsule.progress.mapTo(self.capsule.panel, QPoint(0, 0)).y() + 1, ) def test_timer_and_longest_copy_fit_their_widgets(self): timer_width = QFontMetrics(self.capsule.timer.font()).horizontalAdvance("00:00:00") self.assertLessEqual(timer_width, self.capsule.timer.contentsRect().width()) states = { "running": ("监听中", "安全运行中"), "waiting": ("等待企业微信", "等待企业微信"), "connecting": ("连接中", "正在连接窗口"), "stopping": ("正在停止", "正在结束任务"), "error": ("连接失败", "请查看运行日志"), "verification": ("需要扫码验证", "请先用手机扫码验证"), "stopped": ("已停止", "监听已停止"), } for state, (status, hint) in states.items(): with self.subTest(state=state): self.capsule.set_status(state, status) self.app.processEvents() self.assertEqual(self.capsule.hint.text(), hint) status_width = QFontMetrics( self.capsule.status.font() ).horizontalAdvance(self.capsule.status.text()) hint_width = QFontMetrics( self.capsule.hint.font() ).horizontalAdvance(self.capsule.hint.text()) self.assertLessEqual(status_width, self.capsule.status.width()) self.assertLessEqual(hint_width, self.capsule.hint.width()) self.capsule.timer.setText("99:59:59") timer_width = QFontMetrics(self.capsule.timer.font()).horizontalAdvance( self.capsule.timer.text() ) self.assertLessEqual(timer_width, self.capsule.timer.contentsRect().width()) def test_the_timer_sits_above_the_full_width_progress_line(self): # 缩成胶囊后仍能同时看到时长和完整的当前执行步骤。 timer_top = self.capsule.timer.mapTo(self.capsule.panel, QPoint(0, 0)).y() progress_top = self.capsule.progress.mapTo(self.capsule.panel, QPoint(0, 0)).y() self.assertLess(timer_top + self.capsule.timer.height(), progress_top + 1) def test_the_progress_line_says_who_is_being_answered(self): self.capsule.set_progress("正在回复 高瑞@微信") self.app.processEvents() self.assertEqual(self.capsule.progress.text(), "当前进程 · 正在回复 高瑞@微信") def test_running_without_a_task_still_shows_a_real_process(self): self.capsule.set_status("running", "监听中") self.app.processEvents() self.assertEqual(self.capsule.progress.text(), "当前进程 · 等待新消息") def test_a_long_nickname_is_shortened_instead_of_overflowing(self): long_name = "正在回复 甄养堂医疗助理-盛灵莉客户服务专员@微信" self.capsule.set_progress(long_name) self.app.processEvents() shown = self.capsule.progress.text() self.assertNotEqual(shown, long_name) width = QFontMetrics(self.capsule.progress.font()).horizontalAdvance(shown) self.assertLessEqual(width, self.capsule.progress.width()) # 省略之后还得能查到全名,否则不知道回的是谁 self.assertEqual(self.capsule.progress.toolTip(), f"当前进程 · {long_name}") def test_stopping_clears_a_stale_progress_line(self): # 停下来还挂着"正在回复 XXX"会让人以为它还在干活 self.capsule.set_progress("正在回复 高瑞@微信") self.capsule.set_status("stopped", "已停止") self.app.processEvents() self.assertEqual(self.capsule.progress.text(), "") def test_running_keeps_the_progress_line_alone(self): self.capsule.set_progress("正在读取 高瑞@微信 的消息") self.capsule.set_status("running", "监听中") self.app.processEvents() self.assertEqual( self.capsule.progress.text(), "当前进程 · 正在读取 高瑞@微信 的消息" ) def test_status_copy_and_actions_remain_connected(self): self.capsule.set_status("waiting", "等待企业微信") self.assertEqual(self.capsule.status.text(), "等待企业微信") self.assertEqual(self.capsule.hint.text(), "等待企业微信") events = [] self.capsule.expandRequested.connect(lambda: events.append("expand")) self.capsule.stopRequested.connect(lambda: events.append("stop")) self.capsule.expand_button.click() self.capsule.stop_button.click() self.assertEqual(events, ["expand", "stop"]) self.assertEqual(self.capsule.stop_button.icon_kind, "pause") self.assertEqual(self.capsule.expand_button.icon_kind, "fullscreen") self.assertEqual(self.capsule.stop_button.toolTip(), "停止监听") self.assertEqual(self.capsule.expand_button.toolTip(), "展开控制台") self.assertLess( self.capsule.stop_button.geometry().left(), self.capsule.expand_button.geometry().left(), ) def test_window_flags_and_position_keep_capsule_non_intrusive(self): flags = self.capsule.windowFlags() self.assertTrue(flags & Qt.Tool) self.assertTrue(flags & Qt.FramelessWindowHint) self.assertTrue(flags & Qt.WindowStaysOnTopHint) self.assertTrue(flags & Qt.WindowDoesNotAcceptFocus) self.capsule.show_near(self.capsule) self.app.processEvents() area = self.capsule.screen().availableGeometry() self.assertEqual(self.capsule.y(), area.y() + 28) self.assertEqual( self.capsule.x(), area.x() + (area.width() - self.capsule.width()) // 2, ) @skipUnless(QApplication is not None, "当前测试环境未安装 PySide6") class QueuePageDeleteTest(TestCase): @classmethod def setUpClass(cls): cls.app = QApplication.instance() or QApplication([]) cls.app.setStyleSheet(APP_QSS) def test_delete_action_emits_the_full_untruncated_task_key(self): full_key = "d6904620be23fafcd8774af557daef1118b2b208ceb0655f62bb8d032dd8d0a072ca7bc26546f5e6" with tempfile.TemporaryDirectory() as folder: root = Path(folder) (root / "pending_replies.json").write_text( json.dumps({full_key: {"display_name": "一个小迷糊@微信", "created_at": 1}}), encoding="utf-8", ) with mock.patch("wechat_gui_qt.SCRIPT_DIR", root): page = QueuePage() emitted = [] page.deleteTasksRequested.connect(emitted.append) page.queue_table.selectRow(0) with mock.patch( "wechat_gui_qt.QMessageBox.question", return_value=QMessageBox.Yes, ): page._request_delete_selected() page.deleteLater() self.app.processEvents() self.assertEqual(emitted, [[full_key]]) @skipUnless(QApplication is not None, "当前测试环境未安装 PySide6") class MainWindowMinimizeTest(TestCase): @classmethod def setUpClass(cls): cls.app = QApplication.instance() or QApplication([]) cls.app.setStyleSheet(APP_QSS) def setUp(self): self.window = MainWindow.__new__(MainWindow) QMainWindow.__init__(self.window) self.window.resize(900, 700) self.window._running = False self.window._capsule_minimize_pending = False self.window._saved_geometry = None self.window._was_maximized = False self.window._portal_was_visible = False self.window.capsule = CapsuleWindow() self.window.stack = QStackedWidget() page = QWidget() page_layout = QVBoxLayout(page) portal_view = QWidget() page_layout.addWidget(portal_view) self.window.stack.addWidget(page) self.window.setCentralWidget(self.window.stack) self.window.portal_page = SimpleNamespace(view=portal_view) self.window.show() self.app.processEvents() def tearDown(self): self.window.capsule.close() self.window.hide() self.window.deleteLater() self.app.processEvents() def test_minimize_while_running_switches_to_capsule_and_can_expand(self): self.window._running = True self.window.showMinimized() self.app.processEvents() self.app.processEvents() self.assertTrue(self.window.capsule.isVisible()) self.assertFalse(self.window.isVisible()) self.assertIsNotNone(self.window._saved_geometry) self.window.expand_console() self.app.processEvents() self.assertFalse(self.window.capsule.isVisible()) self.assertTrue(self.window.isVisible()) self.assertFalse(self.window.isMinimized()) def test_minimize_while_stopped_keeps_normal_taskbar_behavior(self): self.window._running = False self.window.showMinimized() self.app.processEvents() self.app.processEvents() self.assertTrue(self.window.isVisible()) self.assertTrue(self.window.isMinimized()) self.assertFalse(self.window.capsule.isVisible()) def test_minimize_from_maximized_restores_maximized_console(self): self.window._running = True self.window.showMaximized() self.app.processEvents() self.assertTrue(self.window.isMaximized()) self.window.showMinimized() self.app.processEvents() self.app.processEvents() self.assertTrue(self.window.capsule.isVisible()) self.assertTrue(self.window._was_maximized) self.window.expand_console() self.app.processEvents() self.assertTrue(self.window.isVisible()) self.assertTrue(self.window.isMaximized()) if __name__ == "__main__": main()