feat(gui): system-tray applet (M11) + GUI smoke tests — 0.19.0

QSystemTrayIcon applet (gui/tray.py, D13): menu with live CPU/GPU temp + memory
used/total, a status line, a Run Diagnostic submenu per detected game, plus Open
dashboard / Start-Stop recording / Snapshot-copy / Quit. Reuses the dashboard's
sample stream; drives existing MainWindow flows.

- MainWindow creates the tray when one is available; closing the window hides to
  tray (Quit exits); setQuitOnLastWindowClosed(False) so dialogs don't quit it.
- app: `--tray` starts hidden for autostart.
- tests/test_gui_smoke.py: construct MainWindow headless + exercise the tray, so
  a startup crash (like the 0.18.0 import bug) fails the build. Skips if no PySide6.
- docs: M10/M11 marked done in MODULES/ROADMAP.

Completes the Desktop UI bundle (M10 + M11).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-22 09:21:37 +02:00
parent f95387c5b8
commit 75a4da7af3
9 changed files with 319 additions and 12 deletions
+68
View File
@@ -0,0 +1,68 @@
"""GUI smoke tests: construct the real widgets so a startup crash fails the build.
These run headless (offscreen) and skip cleanly if PySide6 isn't installed (the core/CLI
test suite stays Qt-free). Constructing MainWindow is the check that would have caught the
0.18.0 bad-import regression that broke launch.
"""
import os
import time
import unittest
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
try:
from PySide6.QtGui import QIcon
from PySide6.QtWidgets import QApplication, QWidget
HAVE_QT = True
except ImportError:
HAVE_QT = False
@unittest.skipUnless(HAVE_QT, "PySide6 not installed")
class GuiSmokeTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.app = QApplication.instance() or QApplication([])
def test_main_window_constructs(self):
from unittest import mock
from rigdoctor.core import updates
from rigdoctor.gui import main_window as mw
# Avoid construction side effects: no pkexec elevation, no network update check.
with mock.patch("rigdoctor.core.elevation.available", return_value=False), \
mock.patch.object(updates, "update_state", return_value=(updates.UP_TO_DATE, None, "")):
window = mw.MainWindow()
try:
self.assertEqual(len(window._nav_buttons), len(mw._NAV_ITEMS))
finally:
window._worker.stop()
def test_tray_readouts_update(self):
from rigdoctor.core.sample import Reading, Sample
from rigdoctor.gui.tray import TrayIcon
class StubWindow(QWidget):
def show_dashboard(self): ...
def show_page(self, name): ...
def run_diagnostic(self, name, appid): ...
def quit_app(self): ...
tray = TrayIcon(StubWindow(), QIcon())
tray.update_sample(Sample(time.time(), [
Reading("gpu", "temp", 72.0, "°C", ""),
Reading("cpu", "temp", 65.0, "°C", "Package id 0"),
Reading("memory", "used", 14.2, "GB"),
Reading("memory", "total", 31.0, "GB"),
Reading("memory", "used_pct", 46.0, "%"),
]))
self.assertIn("72", tray._gpu_act.text())
self.assertIn("65", tray._cpu_act.text())
self.assertIn("14.2 / 31.0 GB", tray._mem_act.text())
self.assertEqual(tray._status_act.text(), "● Normal")
if __name__ == "__main__":
unittest.main()