"""Offscreen lifecycle regressions; no real tray, login, listener or user state.""" import queue import sys from types import SimpleNamespace from unittest import TestCase, mock from PySide6.QtCore import QEvent, Qt from PySide6.QtWidgets import QApplication, QDialog, QMainWindow, QStackedWidget, QWidget import wechat_gui_qt as qt class TrayLifecycleTest(TestCase): @classmethod def setUpClass(cls): cls.app = QApplication.instance() or QApplication([]) def setUp(self): window = self.window = qt.MainWindow.__new__(qt.MainWindow) QMainWindow.__init__(window) window.resize(900, 700) window.runtime_settings = {'minimize_on_close': True, 'clear_clipboard_on_exit': False} window._running = False window._thread = mock.Mock() window._thread.is_alive.return_value = True window._exit_requested = False window._shutdown_complete = False window._tray_hidden = False window._close_minimized = False window._capsule_minimize_pending = False window._saved_geometry = None window._was_maximized = False window._portal_was_visible = False window._console_was_visible = False window._stdout_proxy = None window._original_stdout = sys.stdout window._tray_icon = mock.Mock() window._tray_icon.isVisible.return_value = True window.capsule = qt.CapsuleWindow() window.stack = QStackedWidget() page = QWidget() window.stack.addWidget(page) window.setCentralWidget(window.stack) window.portal_page = SimpleNamespace(view=None) window.console_shell = None window.save_runtime_settings = mock.Mock() window._ensure_system_tray = mock.Mock(return_value=True) window.show() self.app.processEvents() def tearDown(self): self.window.capsule.close() self.window.hide() self.window.deleteLater() self.app.processEvents() def test_stopped_window_closes_to_tray_without_stopping_worker(self): self.window.close() self.assertFalse(self.window.isVisible()) self.assertTrue(self.window._tray_hidden) self.window._thread.stop.assert_not_called() self.window.save_runtime_settings.assert_not_called() self.assertFalse(self.window._shutdown_complete) def test_running_window_closes_to_tray_without_capsule_or_stop(self): self.window._running = True self.window.close() self.assertTrue(self.window._running) self.assertFalse(self.window.capsule.isVisible()) self.window._thread.stop.assert_not_called() def test_tray_setting_outweighs_legacy_keep_background_false(self): self.window.runtime_settings['keep_background'] = False self.window._running = True self.window.close() self.assertTrue(self.window._tray_hidden) self.assertTrue(self.window._running) def test_tray_restore_reopens_without_restarting_listener(self): self.window._running = True self.window.close() self.window.restore_from_tray() self.assertTrue(self.window.isVisible()) self.assertFalse(self.window.isMinimized()) self.assertFalse(self.window._tray_hidden) self.assertTrue(self.window._running) self.window._thread.stop.assert_not_called() def test_tray_restore_preserves_maximized_window(self): self.window.showMaximized() self.app.processEvents() self.window.close() self.window.restore_from_tray() self.assertTrue(self.window.isMaximized()) def test_repeated_activation_keeps_current_visible_geometry(self): self.window._saved_geometry = b'stale' self.window.restoreGeometry = mock.Mock() self.window.restore_from_tray() self.window.restoreGeometry.assert_not_called() def test_running_status_does_not_reopen_capsule_after_tray_close(self): self.window._running = True self.window.close() self.window.enter_capsule_mode() self.assertFalse(self.window.capsule.isVisible()) self.assertFalse(self.window.isVisible()) def test_capsule_closes_to_tray_and_restores_original_geometry(self): self.window._running = True self.window.enter_capsule_mode() geometry = self.window._saved_geometry self.window.close() self.assertFalse(self.window.capsule.isVisible()) self.assertEqual(self.window._saved_geometry, geometry) self.window.restore_from_tray() self.assertTrue(self.window.isVisible()) def test_no_tray_falls_back_to_taskbar_even_when_running(self): self.window._running = True self.window._ensure_system_tray.return_value = False self.window.close() self.app.processEvents() self.assertTrue(self.window.isVisible()) self.assertTrue(self.window.isMinimized()) self.assertFalse(self.window.capsule.isVisible()) self.assertFalse(self.window._tray_hidden) self.window._thread.stop.assert_not_called() self.window.restore_from_tray() self.assertTrue(self.window.isVisible()) self.assertFalse(self.window.isMinimized()) def test_taskbar_native_restore_clears_fallback_mode(self): self.window._ensure_system_tray.return_value = False self.window.close() self.window.showNormal() self.assertFalse(self.window._close_minimized) def test_no_tray_fallback_restores_capsule_hidden_surfaces(self): self.window.console_shell = QWidget(self.window) self.window.console_shell.show() self.window.enter_capsule_mode() self.window._ensure_system_tray.return_value = False self.window.close() self.assertTrue(self.window.isMinimized()) self.assertFalse(self.window.console_shell.isHidden()) def test_about_to_quit_is_wired_to_cleanup(self): self.window._shutdown_application = mock.Mock() with mock.patch.object(sys, 'argv', ['test', '--qt-smoke-test']): self.window._initialize_system_tray() try: self.app.aboutToQuit.emit() self.window._shutdown_application.assert_called_once() finally: self.app.aboutToQuit.disconnect(self.window._shutdown_application) self.app.removeEventFilter(self.window._quit_filter) self.window._quit_filter.deleteLater() def test_explicit_disable_close_to_tray_quits(self): self.window.runtime_settings['minimize_on_close'] = False self.window._shutdown_application = mock.Mock() with mock.patch.object(QApplication, 'quit') as quit_app: self.window.close() self.window._shutdown_application.assert_called_once() quit_app.assert_called_once() self.window._ensure_system_tray.assert_not_called() def test_forced_relogin_restores_tray_hidden_web_surface(self): self.window.console_shell = QWidget(self.window) self.window.console_shell.show() self.window.close() self.window._forced_logout_handled = False self.window.desktop_session_timer = mock.Mock() self.window.stop_monitoring = mock.Mock() self.window.account_menu = mock.Mock() self.window._check_desktop_session = mock.Mock() self.window.persona_page = mock.Mock() modules = {'backend_client': SimpleNamespace(desktop_logout=mock.Mock()), 'desktop_login': SimpleNamespace(ensure_desktop_login=mock.Mock(return_value={'nickname': 'test'}))} with mock.patch.dict(sys.modules, modules): with mock.patch.object(qt.QMessageBox, 'warning'), mock.patch.object(qt.QTimer, 'singleShot'): self.window._handle_forced_desktop_logout('test logout') self.assertFalse(self.window._tray_hidden) self.assertTrue(self.window.isVisible()) self.assertFalse(self.window.console_shell.isHidden()) def test_system_logoff_does_not_close_to_tray(self): self.window._shutdown_application = mock.Mock() with mock.patch.object(QApplication, 'isSavingSession', return_value=True): with mock.patch.object(QApplication, 'quit'): self.window.close() self.assertTrue(self.window._exit_requested) self.window._shutdown_application.assert_called_once() self.window._ensure_system_tray.assert_not_called() def test_exit_menu_bypasses_close_to_tray(self): self.window._shutdown_application = mock.Mock() with mock.patch.object(QApplication, 'quit') as quit_app: self.window.request_exit() self.assertTrue(self.window._exit_requested) self.window._shutdown_application.assert_called_once() self.window._ensure_system_tray.assert_not_called() self.assertTrue(quit_app.called) def test_cleanup_stops_listener_and_backup_once(self): backup = mock.Mock() desk = mock.Mock() self.window.queue_timer = mock.Mock() self.window._stdout_proxy = mock.Mock() proxy = self.window._stdout_proxy with mock.patch.dict(sys.modules, {'archive_auto_backup': SimpleNamespace(stop_auto_backup=backup), 'dsh_agent': SimpleNamespace(close_desk=desk)}): self.window._shutdown_application() self.window._shutdown_application() self.window._thread.stop.assert_called_once() self.window.save_runtime_settings.assert_called_once() self.window.queue_timer.stop.assert_called_once() proxy.close.assert_called_once() backup.assert_called_once() desk.assert_called_once() self.window._tray_icon.hide.assert_called_once() def test_save_failure_does_not_prevent_worker_shutdown(self): self.window.save_runtime_settings.side_effect = OSError('test disk failure') with mock.patch.dict(sys.modules, {'archive_auto_backup': SimpleNamespace(stop_auto_backup=mock.Mock()), 'dsh_agent': SimpleNamespace(close_desk=mock.Mock())}): self.window._shutdown_application() self.window._thread.stop.assert_called_once() def test_application_quit_marks_window_for_real_close(self): event_filter = qt._ApplicationQuitFilter(self.window) self.assertFalse(event_filter.eventFilter(self.app, QEvent(QEvent.Type.Quit))) self.assertTrue(self.window._exit_requested) def test_single_and_double_click_restore_but_context_menu_does_not(self): self.window.restore_from_tray = mock.Mock() reasons = qt.QSystemTrayIcon.ActivationReason self.window._tray_activated(reasons.Context) self.window.restore_from_tray.assert_not_called() self.window._tray_activated(reasons.Trigger) self.window._tray_activated(reasons.DoubleClick) self.assertEqual(self.window.restore_from_tray.call_count, 2) def test_tray_menu_has_explicit_open_and_exit_actions(self): self.window._tray_icon = None self.window.restore_from_tray = mock.Mock() self.window.request_exit = mock.Mock() with mock.patch.object(qt, 'QSystemTrayIcon') as tray_class: tray_class.isSystemTrayAvailable.return_value = True tray_class.return_value.isVisible.return_value = True self.assertTrue(qt.MainWindow._ensure_system_tray(self.window)) actions = self.window._tray_menu.actions() self.assertEqual([action.text() for action in actions if not action.isSeparator()], ['打开主界面', '退出程序']) actions[0].trigger() actions[-1].trigger() self.window.restore_from_tray.assert_called_once() self.window.request_exit.assert_called_once() def test_console_interrupt_uses_real_exit_not_window_close(self): with mock.patch.object(qt.signal, 'signal'), mock.patch.object(qt.QTimer, 'singleShot') as single: shutdown = qt.install_console_shutdown_handler(self.app, self.window) shutdown() shutdown() single.assert_called_once_with(0, self.window.request_exit) class InstanceActivationTest(TestCase): @classmethod def setUpClass(cls): cls.app = QApplication.instance() or QApplication([]) def test_activation_waits_until_ui_exists(self): app = SimpleNamespace(activeModalWidget=lambda: None, activeWindow=lambda: None, topLevelWidgets=lambda: []) guard = mock.Mock() qt._consume_instance_activation(app, guard) guard.consume_activation.assert_not_called() def test_login_modal_activated_before_main_window(self): dialog = QDialog() self.addCleanup(dialog.deleteLater) dialog.showMinimized() app = SimpleNamespace(activeModalWidget=lambda: dialog) window = mock.Mock() guard = mock.Mock() guard.consume_activation.return_value = True qt._consume_instance_activation(app, guard, window) self.assertTrue(dialog.isVisible()) self.assertFalse(dialog.isMinimized()) window.restore_from_tray.assert_not_called() dialog.hide() def test_no_signal_does_not_change_window(self): dialog = QDialog() self.addCleanup(dialog.deleteLater) dialog.showMinimized() app = SimpleNamespace(activeModalWidget=lambda: dialog) guard = mock.Mock() guard.consume_activation.return_value = False qt._consume_instance_activation(app, guard) self.assertTrue(dialog.isMinimized()) dialog.hide() def _main(self, *, primary=True, exception=None, flags=(), session_error=None): guard = mock.Mock() guard.acquire.return_value = primary guard.acquire.side_effect = exception constructor = mock.Mock(return_value=guard) session = mock.Mock(side_effect=session_error) with mock.patch.dict(sys.modules, {'desktop_instance': SimpleNamespace(DesktopInstanceGuard=constructor)}), \ mock.patch.object(sys, 'argv', ['wechat_gui.py', *flags]), \ mock.patch.object(qt, '_run_desktop_session', session), \ mock.patch.object(qt.QMessageBox, 'critical') as error: try: qt.main() except ValueError: pass return guard, constructor, session, error def test_secondary_does_not_enter_login_or_construct_main_window(self): guard, _, session, _ = self._main(primary=False) session.assert_not_called() guard.request_activation.assert_called_once() guard.close.assert_called_once() def test_primary_enters_session_and_releases_guard(self): guard, _, session, _ = self._main() session.assert_called_once() guard.request_activation.assert_not_called() guard.close.assert_called_once() def test_guard_failure_never_starts_unguarded_client(self): guard, _, session, error = self._main(exception=OSError('mock unavailable')) session.assert_not_called() error.assert_called_once() guard.close.assert_called_once() def test_guard_constructor_failure_is_reported_without_login(self): constructor = mock.Mock(side_effect=OSError('mock token unavailable')) with mock.patch.dict(sys.modules, {'desktop_instance': SimpleNamespace(DesktopInstanceGuard=constructor)}): with mock.patch.object(sys, 'argv', ['wechat_gui.py']), mock.patch.object(qt, '_run_desktop_session') as session: with mock.patch.object(qt.QMessageBox, 'critical') as error: qt.main() error.assert_called_once() session.assert_not_called() def test_session_exception_releases_guard(self): guard, _, _, _ = self._main(session_error=ValueError('test')) guard.close.assert_called_once() def test_smoke_test_does_not_contact_real_instance(self): guard, constructor, session, _ = self._main(flags=['--qt-smoke-test']) constructor.assert_not_called() guard.acquire.assert_not_called() session.assert_called_once() def test_packaging_selfcheck_does_not_contact_real_instance(self): with mock.patch.object(qt, 'run_packaging_self_check', return_value=0): with self.assertRaises(SystemExit): self._main(flags=['--packaging-self-check'])