diff --git a/src/vocalinux/ui/logging_dialog.py b/src/vocalinux/ui/logging_dialog.py index 8eb73a0..2f18345 100644 --- a/src/vocalinux/ui/logging_dialog.py +++ b/src/vocalinux/ui/logging_dialog.py @@ -622,9 +622,9 @@ class LoggingDialog(Gtk.Dialog): return False # Remove from idle queue - def _on_response(self, dialog, response_id): + def _on_response(self, dialog: Gtk.Dialog, response_id: int) -> None: """Handle dialog responses.""" - if response_id == Gtk.ResponseType.CLOSE: + if response_id in (Gtk.ResponseType.CLOSE, Gtk.ResponseType.DELETE_EVENT): self.destroy() def _export_logs(self): diff --git a/tests/test_logging_dialog.py b/tests/test_logging_dialog.py index b338b61..90ab7bf 100644 --- a/tests/test_logging_dialog.py +++ b/tests/test_logging_dialog.py @@ -94,6 +94,50 @@ class TestLoggingDialogClass(unittest.TestCase): """Test that dialog unregisters callback on destroy.""" self.assertIn("unregister_callback(self._on_new_log_record)", self.source_code) + def test_response_handler_closes_on_delete_event(self) -> None: + """Titlebar-X (DELETE_EVENT) destroys the dialog like Close does.""" + import ast + from types import SimpleNamespace + from unittest.mock import MagicMock + + # conftest mocks gi globally, so `class LoggingDialog(Gtk.Dialog)` + # resolves to a Mock at import time and the real handler is + # unreachable. Compile the actual _on_response source node and exec + # it against a real Gtk namespace — this runs the true code path. + class _FakeGtk: + Dialog = object + ResponseType = SimpleNamespace(CLOSE=-7, DELETE_EVENT=-4, CANCEL=-6) + + def __getattr__(self, _name: str) -> type: + return object + + tree = ast.parse(self.source_code) + func_node = next( + node + for cls in tree.body + if isinstance(cls, ast.ClassDef) and cls.name == "LoggingDialog" + for node in cls.body + if isinstance(node, ast.FunctionDef) and node.name == "_on_response" + ) + module = ast.Module(body=[func_node], type_ignores=[]) + namespace: dict = {"Gtk": _FakeGtk()} + exec(compile(ast.fix_missing_locations(module), "", "exec"), namespace) + handler = namespace["_on_response"] + + dialog = SimpleNamespace() + for response_id, expected in ( + (_FakeGtk.ResponseType.CLOSE, True), + (_FakeGtk.ResponseType.DELETE_EVENT, True), + (_FakeGtk.ResponseType.CANCEL, False), + ): + dialog.destroy = MagicMock() + handler(dialog, dialog, response_id) + self.assertIs( + dialog.destroy.called, + expected, + f"response {response_id}: destroy called={dialog.destroy.called}", + ) + class TestLoggingDialogFilterBar(unittest.TestCase): """Test cases for LoggingDialog filter bar."""