Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,44 @@
"""Add parent directory to sys.path for imports."""
import os
import sys
from pathlib import Path

import pytest

os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")

sys.path.insert(0, str(Path(__file__).parent.parent))


@pytest.fixture(scope="session")
def qapp():
"""Keep one Qt application alive and dispose test-owned Qt UI before shutdown."""
from PySide6.QtCore import QCoreApplication, QEvent
from PySide6.QtWidgets import QApplication, QSystemTrayIcon

app = QApplication.instance()
if app is None:
app = QApplication([])
assert isinstance(app, QApplication)

yield app

tray_icons = tuple(app.findChildren(QSystemTrayIcon))
widgets = list(app.topLevelWidgets())
for icon in tray_icons:
menu = icon.contextMenu()
if menu is not None and menu not in widgets:
widgets.append(menu)
icon.hide()
icon.setContextMenu(None)

for widget in widgets:
widget.close()
widget.deleteLater()
for icon in tray_icons:
icon.deleteLater()

QCoreApplication.sendPostedEvents(None, QEvent.Type.DeferredDelete)

assert not app.topLevelWidgets(), "Qt top-level widgets remained after session cleanup"
assert not app.findChildren(QSystemTrayIcon), "Qt tray icons remained after session cleanup"
2 changes: 2 additions & 0 deletions tests/test_assets_and_icons.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from pathlib import Path

from PIL import Image
import pytest

ROOT = Path(__file__).resolve().parent.parent
if str(ROOT) not in sys.path:
Expand All @@ -32,6 +33,7 @@ def get_ico_sizes(ico_path: Path) -> list[tuple[int, int]]:
return sorted(sizes)


@pytest.mark.usefixtures("qapp")
class TestMailProcessorIconsAndAssets(unittest.TestCase):
"""Testet Existenz, Dimensionen und Layer-Vollständigkeit aller Icons."""

Expand Down
8 changes: 2 additions & 6 deletions tests/test_bundle_selftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,13 @@
from pathlib import Path

import pytest
from PySide6.QtWidgets import QApplication

import config as cfg_module
import main as main_module


@pytest.fixture
def isolated_qapp(monkeypatch, tmp_path):
def isolated_qapp(qapp, monkeypatch, tmp_path):
profile = tmp_path / "profile"
monkeypatch.setenv("QT_QPA_PLATFORM", "offscreen")
monkeypatch.setenv("LOCALAPPDATA", str(profile / "localappdata"))
Expand All @@ -21,10 +20,7 @@ def isolated_qapp(monkeypatch, tmp_path):
monkeypatch.setenv("TEMP", str(profile / "temp"))
monkeypatch.setenv("TMP", str(profile / "temp"))

app = QApplication.instance()
if app is None:
app = QApplication([])
return app, profile
return qapp, profile


def test_self_test_checks_frozen_ui_without_loading_or_saving_profile(
Expand Down
15 changes: 2 additions & 13 deletions tests/test_installer.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,14 @@
"""Tests for installer.py - wizard state and download tracking."""

import os

import pytest
from PySide6.QtWidgets import QApplication, QDialog, QPushButton
from PySide6.QtWidgets import QDialog, QPushButton

from config import AppConfig, ToolConfig
from i18n import set_language
from installer import PathsPage, ToolsPage, WelcomePage
from tool_manager import ToolManager


@pytest.fixture
def qapp():
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
app = QApplication.instance()
if app is None:
app = QApplication([])
return app


@pytest.fixture
def tm(monkeypatch):
cfg = AppConfig()
Expand Down Expand Up @@ -55,7 +44,7 @@ def _fake_exec(self):

monkeypatch.setattr(SettingsDialog, "exec", _fake_exec)

tray = MailProcessorTray(cfg)
tray = MailProcessorTray(cfg, parent=qapp)
tray._open_settings()

assert cfg.tools["universal_mail_cleaner"].enabled is True, (
Expand Down
14 changes: 1 addition & 13 deletions tests/test_settings_dialog.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,10 @@
"""Tests for settings_dialog.py — rescan feedback and tool management."""
import os

import pytest
from PySide6.QtWidgets import QApplication, QMessageBox
from PySide6.QtWidgets import QMessageBox

from config import AppConfig, ToolConfig
from i18n import set_language


@pytest.fixture
def qapp():
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
app = QApplication.instance()
if app is None:
app = QApplication([])
return app


def test_rescan_shows_rescan_done_when_tools_added(qapp, tmp_path, monkeypatch):
"""_on_rescan() shows 'rescan_done' message when new tools are detected."""
set_language("de")
Expand Down
23 changes: 7 additions & 16 deletions tests/test_tray_icon.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
from __future__ import annotations

import hashlib
import os
import struct
from pathlib import Path

os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")

from PySide6.QtWidgets import QApplication
from PySide6.QtWidgets import QMenu, QSystemTrayIcon

import tray

Expand All @@ -34,13 +31,11 @@ def _read_ico_sizes(path: Path) -> set[tuple[int, int]]:
return sizes


def test_runtime_icon_loads_from_file_instead_of_fallback():
app = QApplication.instance() or QApplication([])

def test_runtime_icon_loads_from_file_instead_of_fallback(qapp):
icon = tray._load_tray_icon()
fallback = tray._make_tray_icon()

assert app is not None
assert qapp is not None
assert not icon.isNull()
assert icon.cacheKey() != fallback.cacheKey()

Expand All @@ -58,7 +53,7 @@ def test_runtime_icon_contains_expected_sizes():
assert {(16, 16), (32, 32), (48, 48), (256, 256)}.issubset(sizes)


def test_pyside6_keeps_context_menu_alive_after_gc():
def test_pyside6_keeps_context_menu_alive_after_gc(qapp):
"""Dokumentiert: PySide6 hält QMenu intern am Leben nach setContextMenu().

Auch ohne Python-Instanzvariable (lokale Variable) überlebt das QMenu gc.collect().
Expand All @@ -67,10 +62,7 @@ def test_pyside6_keeps_context_menu_alive_after_gc():
"""
import gc

from PySide6.QtWidgets import QMenu, QSystemTrayIcon

_app = QApplication.instance() or QApplication([])
icon = QSystemTrayIcon()
icon = QSystemTrayIcon(qapp)
local_menu = QMenu()
local_menu.addAction("MarkAction")
icon.setContextMenu(local_menu)
Expand All @@ -82,14 +74,13 @@ def test_pyside6_keeps_context_menu_alive_after_gc():
assert len(result.actions()) > 0, "Aktionen bleiben nach GC erhalten"


def test_build_menu_stores_menu_as_instance_variable():
def test_build_menu_stores_menu_as_instance_variable(qapp):
"""_build_menu muss self._context_menu setzen damit Python-GC das QMenu nicht löscht."""
from config import AppConfig
from tray import MailProcessorTray

_app = QApplication.instance() or QApplication([])
cfg = AppConfig()
tray_icon = MailProcessorTray(cfg)
tray_icon = MailProcessorTray(cfg, parent=qapp)

assert hasattr(tray_icon, "_context_menu"), "_context_menu muss als Instanzvariable gehalten werden"
assert tray_icon._context_menu is not None
Expand Down
Loading