Skip to content
Open
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
80 changes: 80 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
"""Shared pytest setup and teardown for ExplorerPro.

1. Safe Symlink & Numbered Dir Cleanup (Windows):
On Windows, pytest's `cleanup_dead_symlinks` in `_pytest.pathlib` checks
`left_dir.resolve().exists()` on `pytest-current` (a directory symlink).
Without Developer Mode or when symlink permissions differ, `exists()` raises
`PermissionError: [WinError 5] Zugriff verweigert: '...\\pytest-current'`.
When raised inside `tmp_path_factory._exit_stack.close()` at sessionfinish,
CPython aborts with `Fatal Python error: Aborted` / `STATUS_STACK_BUFFER_OVERRUN`
(exit code 1). Suppressing OSError in `cleanup_dead_symlinks` and `cleanup_numbered_dir`
guards against this Windows-specific teardown failure.

2. Clean Qt Top-Level Widget Teardown:
Closes and schedules `deleteLater()` for remaining top-level Qt widgets
and processes pending events, preventing dangling QObject destructors
at interpreter exit.
"""
from __future__ import annotations

import os

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

try:
import _pytest.pathlib as _pytest_pathlib

_orig_cleanup_dead_symlinks = _pytest_pathlib.cleanup_dead_symlinks
_orig_cleanup_numbered_dir = _pytest_pathlib.cleanup_numbered_dir

def _safe_cleanup_dead_symlinks(root):
try:
_orig_cleanup_dead_symlinks(root)
except OSError:
pass

def _safe_cleanup_numbered_dir(root, prefix, keep, consider_lock_dead_if_created_before):
try:
_orig_cleanup_numbered_dir(root, prefix, keep, consider_lock_dead_if_created_before)
except OSError:
pass

_pytest_pathlib.cleanup_dead_symlinks = _safe_cleanup_dead_symlinks
_pytest_pathlib.cleanup_numbered_dir = _safe_cleanup_numbered_dir
except (ImportError, AttributeError):
pass

import pytest
from PySide6.QtWidgets import QApplication


@pytest.fixture(autouse=True)
def _cleanup_qt_widgets():
"""Ensure top-level Qt widgets created during a test are closed and drained."""
yield
app = QApplication.instance()
if app is not None:
for widget in app.topLevelWidgets():
try:
widget.close()
widget.deleteLater()
except Exception:
pass
app.processEvents()


def pytest_sessionfinish(session, exitstatus):
"""Drain any remaining events before interpreter shutdown."""
app = QApplication.instance()
if app is not None:
try:
for widget in app.topLevelWidgets():
try:
widget.close()
widget.deleteLater()
except Exception:
pass
for _ in range(5):
app.processEvents()
except Exception:
pass
5 changes: 4 additions & 1 deletion tests/test_checksum_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,13 @@ def test_checksum_dialog_non_existent_file(tmp_path: Path):

def test_checksum_dialog_close_cancels_worker(tmp_path: Path):
"""Prüft, dass closeEvent den Worker sauber abbricht."""
_ensure_app()
app = _ensure_app()
sample = tmp_path / "cancel_test.txt"
sample.write_bytes(b"X" * (500 * 1024))

dlg = ChecksumDialog(str(sample))
dlg.closeEvent(QCloseEvent())
assert dlg.worker is not None
dlg.close()
dlg.deleteLater()
app.processEvents()
45 changes: 45 additions & 0 deletions tests/test_store_regressions_20260928.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,3 +254,48 @@ def test_frozen_run_button_starts_interpreter_not_explorerpro(tmp_path, monkeypa
editor._run_code()
assert launched == [(str(interpreter), [str(script)])]
editor.close()


def test_right_click_on_unselected_row_selects_row(tmp_path, monkeypatch):
_app()
first = tmp_path / "first.txt"
second = tmp_path / "second.txt"
first.write_text("first", encoding="utf-8")
second.write_text("second", encoding="utf-8")

browser = FileBrowser()
browser.resize(800, 500)
browser.show()
browser.navigate_to(str(tmp_path))
for _ in range(100):
_app().processEvents()
if browser.proxy.rowCount(browser.table.rootIndex()) == 2:
break
time.sleep(.01)

idx_first = browser.proxy.mapFromSource(browser.model.index(str(first)))
browser.table.selectRow(idx_first.row())
assert [Path(p) for p in browser.get_selected_files()] == [first]

idx_second = browser.proxy.mapFromSource(browser.model.index(str(second)))
assert not browser.table.selectionModel().isRowSelected(idx_second.row(), idx_second.parent())

menus = []

class CapturingMenu(QMenu):
def exec(self, *args):
menus.append(self)

monkeypatch.setattr(browser_module, "QMenu", CapturingMenu)
pos = browser.table.visualRect(idx_second).center()
event = QContextMenuEvent(
QContextMenuEvent.Reason.Mouse, pos, browser.table.viewport().mapToGlobal(pos)
)
QApplication.sendEvent(browser.table.viewport(), event)

assert browser.table.selectionModel().isRowSelected(idx_second.row(), idx_second.parent())
assert second in [Path(p) for p in browser.get_selected_files()]

browser.close()
browser.deleteLater()
_app().processEvents()
Loading