* fix(ui): use Gtk response signal for first-run dialog Overriding do_response and calling Gtk.Dialog.do_response() breaks on modern PyGObject with "Class GtkDialog doesn't implement response". Connect to the response signal instead so dialog.run() still works. Fixes #566 * test(ui): cover first-run dialog response handler Exercise connect("response") wiring and _on_response mapping so codecov/patch is green for the #566 Gtk.Dialog.do_response fix. * test(ui): isolate first-run dialog response coverage Load first_run_dialog under a unique module name with a real Gtk.Dialog stand-in so _on_response and connect("response") run under full-suite import pollution without clobbering other tests' gi mocks. * test(ui): cover _on_response via SimpleNamespace Call FirstRunDialog._on_response on a stand-in object instead of constructing Gtk.Dialog; enough for codecov/patch on the handler body. * test(ui): map first-run responses via SimpleNamespace Exercise FirstRunDialog._on_response for YES/NO/LATER/unknown without constructing Gtk.Dialog. * test(ui): collapse first-run response cases into one table One loop covers YES/NO/LATER/unknown instead of four copy-pasted tests. * test(ui): restore isolated first-run dialog coverage load SimpleNamespace calls against vocalinux.ui.first_run_dialog.FirstRunDialog fail under the full suite when earlier tests leave the class as a MagicMock. Reload the source under a unique module name with a real Gtk.Dialog stand-in (as in 680c5ed) so _on_response and connect("response") stay green on CI.
317 lines
11 KiB
Python
317 lines
11 KiB
Python
"""
|
|
Final execution-based tests for FirstRunDialog.
|
|
|
|
These tests focus on actually exercising the code with proper mocking.
|
|
"""
|
|
|
|
import importlib.util
|
|
import sys
|
|
import types
|
|
import unittest
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
# Mock gi and Gtk before importing first_run_dialog (constants / show_* tests).
|
|
# Response-handler tests reinstall a real Dialog base and reload the module so
|
|
# they stay correct even when earlier tests left FirstRunDialog as a MagicMock.
|
|
mock_gi = MagicMock()
|
|
mock_gtk = MagicMock()
|
|
sys.modules["gi"] = mock_gi
|
|
sys.modules["gi.repository"] = MagicMock()
|
|
sys.modules["gi.repository.Gtk"] = mock_gtk
|
|
|
|
|
|
class _FakeGtkDialog:
|
|
"""Minimal Gtk.Dialog stand-in for unit tests."""
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
self._connect_calls = []
|
|
|
|
def set_default_size(self, *args, **kwargs):
|
|
pass
|
|
|
|
def connect(self, *args, **kwargs):
|
|
self._connect_calls.append((args, kwargs))
|
|
return 0
|
|
|
|
def get_content_area(self):
|
|
return MagicMock()
|
|
|
|
def add_button(self, *args, **kwargs):
|
|
return MagicMock()
|
|
|
|
def get_action_area(self):
|
|
return MagicMock()
|
|
|
|
def show_all(self):
|
|
pass
|
|
|
|
|
|
_ISOLATED_MODULE_NAME = "vocalinux_test_first_run_dialog_isolated"
|
|
|
|
|
|
def _load_first_run_dialog_with_fake_gtk():
|
|
"""Load first_run_dialog.py under an isolated module name with a real Dialog base.
|
|
|
|
Other test modules often leave gi/Gtk/FirstRunDialog as MagicMocks. Loading
|
|
the source file under a unique name (and restoring gi afterward) lets
|
|
__init__ / _on_response run without clobbering the suite's import graph.
|
|
Coverage still attributes execution to first_run_dialog.py by file path.
|
|
"""
|
|
mock_gi_local = MagicMock()
|
|
mock_gtk_local = MagicMock()
|
|
mock_gtk_local.Dialog = _FakeGtkDialog
|
|
mock_gtk_local.DialogFlags.MODAL = 1
|
|
mock_gtk_local.ResponseType.DELETE_EVENT = -4
|
|
mock_gtk_local.Justification.CENTER = 0
|
|
mock_gtk_local.Justification.LEFT = 1
|
|
mock_gtk_local.Orientation.HORIZONTAL = 0
|
|
mock_gtk_local.Align.CENTER = 0
|
|
mock_gtk_local.Label = MagicMock
|
|
mock_gtk_local.Box = MagicMock
|
|
mock_gtk_local.Window = MagicMock
|
|
|
|
mock_repo = types.ModuleType("gi.repository")
|
|
mock_repo.Gtk = mock_gtk_local
|
|
|
|
gi_keys = ("gi", "gi.repository", "gi.repository.Gtk")
|
|
saved = {key: sys.modules.get(key) for key in gi_keys}
|
|
try:
|
|
sys.modules["gi"] = mock_gi_local
|
|
sys.modules["gi.repository"] = mock_repo
|
|
sys.modules["gi.repository.Gtk"] = mock_gtk_local
|
|
|
|
path = (
|
|
Path(__file__).resolve().parents[1] / "src" / "vocalinux" / "ui" / "first_run_dialog.py"
|
|
)
|
|
spec = importlib.util.spec_from_file_location(_ISOLATED_MODULE_NAME, path)
|
|
module = importlib.util.module_from_spec(spec)
|
|
sys.modules[_ISOLATED_MODULE_NAME] = module
|
|
assert spec.loader is not None
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
finally:
|
|
for key, previous in saved.items():
|
|
if previous is None:
|
|
sys.modules.pop(key, None)
|
|
else:
|
|
sys.modules[key] = previous
|
|
|
|
|
|
class TestFirstRunDialogConstants(unittest.TestCase):
|
|
"""Tests for FirstRunDialog constants."""
|
|
|
|
def test_response_yes_constant(self):
|
|
"""Test that RESPONSE_YES constant is defined."""
|
|
from vocalinux.ui.first_run_dialog import RESPONSE_YES
|
|
|
|
self.assertEqual(RESPONSE_YES, 1)
|
|
|
|
def test_response_no_constant(self):
|
|
"""Test that RESPONSE_NO constant is defined."""
|
|
from vocalinux.ui.first_run_dialog import RESPONSE_NO
|
|
|
|
self.assertEqual(RESPONSE_NO, 2)
|
|
|
|
def test_response_later_constant(self):
|
|
"""Test that RESPONSE_LATER constant is defined."""
|
|
from vocalinux.ui.first_run_dialog import RESPONSE_LATER
|
|
|
|
self.assertEqual(RESPONSE_LATER, 3)
|
|
|
|
def test_response_constants_are_distinct(self):
|
|
"""Test that response constants have unique values."""
|
|
from vocalinux.ui.first_run_dialog import RESPONSE_LATER, RESPONSE_NO, RESPONSE_YES
|
|
|
|
constants = [RESPONSE_YES, RESPONSE_NO, RESPONSE_LATER]
|
|
self.assertEqual(len(constants), len(set(constants)))
|
|
|
|
|
|
class TestFirstRunDialogResponseHandler(unittest.TestCase):
|
|
"""Tests for response signal wiring and _on_response mapping."""
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
cls.mod = _load_first_run_dialog_with_fake_gtk()
|
|
cls.FirstRunDialog = cls.mod.FirstRunDialog
|
|
cls.RESPONSE_YES = cls.mod.RESPONSE_YES
|
|
cls.RESPONSE_NO = cls.mod.RESPONSE_NO
|
|
cls.RESPONSE_LATER = cls.mod.RESPONSE_LATER
|
|
|
|
@classmethod
|
|
def tearDownClass(cls):
|
|
sys.modules.pop(_ISOLATED_MODULE_NAME, None)
|
|
|
|
def test_connects_response_signal_to_handler(self):
|
|
"""Constructor wires the response signal instead of do_response."""
|
|
dialog = self.FirstRunDialog()
|
|
|
|
self.assertTrue(dialog._connect_calls)
|
|
signal_name, handler = dialog._connect_calls[0][0][:2]
|
|
self.assertEqual(signal_name, "response")
|
|
self.assertTrue(callable(handler))
|
|
# Bound method of this instance (each attribute access makes a new object)
|
|
self.assertIs(handler.__self__, dialog)
|
|
self.assertIs(handler.__func__, self.FirstRunDialog._on_response)
|
|
|
|
def test_on_response_maps_yes(self):
|
|
"""YES response maps to 'yes'."""
|
|
dialog = self.FirstRunDialog()
|
|
dialog._on_response(dialog, self.RESPONSE_YES)
|
|
self.assertEqual(dialog.result, "yes")
|
|
|
|
def test_on_response_maps_no(self):
|
|
"""NO response maps to 'no'."""
|
|
dialog = self.FirstRunDialog()
|
|
dialog._on_response(dialog, self.RESPONSE_NO)
|
|
self.assertEqual(dialog.result, "no")
|
|
|
|
def test_on_response_maps_later(self):
|
|
"""LATER response maps to 'later'."""
|
|
dialog = self.FirstRunDialog()
|
|
dialog._on_response(dialog, self.RESPONSE_LATER)
|
|
self.assertEqual(dialog.result, "later")
|
|
|
|
def test_on_response_maps_delete_event_to_none(self):
|
|
"""DELETE_EVENT maps to None (dialog closed without a choice)."""
|
|
dialog = self.FirstRunDialog()
|
|
dialog.result = "yes" # ensure handler overwrites prior value
|
|
dialog._on_response(dialog, self.mod.Gtk.ResponseType.DELETE_EVENT)
|
|
self.assertIsNone(dialog.result)
|
|
|
|
def test_on_response_unknown_id_is_none(self):
|
|
"""Unknown response IDs map to None via dict.get default."""
|
|
dialog = self.FirstRunDialog()
|
|
dialog.result = "yes"
|
|
dialog._on_response(dialog, 99999)
|
|
self.assertIsNone(dialog.result)
|
|
|
|
def test_on_response_matches_response_map(self):
|
|
"""Each entry in _response_map is applied by _on_response."""
|
|
dialog = self.FirstRunDialog()
|
|
for response_id, expected in dialog._response_map.items():
|
|
dialog._on_response(dialog, response_id)
|
|
self.assertEqual(dialog.result, expected)
|
|
|
|
|
|
class TestShowFirstRunDialogFunction(unittest.TestCase):
|
|
"""Tests for show_first_run_dialog function."""
|
|
|
|
def test_show_first_run_dialog_function_exists(self):
|
|
"""Test that show_first_run_dialog function exists."""
|
|
from vocalinux.ui.first_run_dialog import show_first_run_dialog
|
|
|
|
self.assertIsNotNone(show_first_run_dialog)
|
|
self.assertTrue(callable(show_first_run_dialog))
|
|
|
|
@patch("vocalinux.ui.first_run_dialog.FirstRunDialog")
|
|
def test_show_first_run_dialog_creates_dialog(self, mock_dialog_class):
|
|
"""Test that show_first_run_dialog creates a FirstRunDialog."""
|
|
from vocalinux.ui.first_run_dialog import show_first_run_dialog
|
|
|
|
mock_dialog = MagicMock()
|
|
mock_dialog.result = "yes"
|
|
mock_dialog_class.return_value = mock_dialog
|
|
|
|
show_first_run_dialog()
|
|
|
|
mock_dialog_class.assert_called_once()
|
|
|
|
@patch("vocalinux.ui.first_run_dialog.FirstRunDialog")
|
|
def test_show_first_run_dialog_runs_dialog(self, mock_dialog_class):
|
|
"""Test that show_first_run_dialog calls run()."""
|
|
from vocalinux.ui.first_run_dialog import show_first_run_dialog
|
|
|
|
mock_dialog = MagicMock()
|
|
mock_dialog.result = "yes"
|
|
mock_dialog_class.return_value = mock_dialog
|
|
|
|
show_first_run_dialog()
|
|
|
|
mock_dialog.run.assert_called_once()
|
|
|
|
@patch("vocalinux.ui.first_run_dialog.FirstRunDialog")
|
|
def test_show_first_run_dialog_destroys_dialog(self, mock_dialog_class):
|
|
"""Test that show_first_run_dialog calls destroy()."""
|
|
from vocalinux.ui.first_run_dialog import show_first_run_dialog
|
|
|
|
mock_dialog = MagicMock()
|
|
mock_dialog.result = "yes"
|
|
mock_dialog_class.return_value = mock_dialog
|
|
|
|
show_first_run_dialog()
|
|
|
|
mock_dialog.destroy.assert_called_once()
|
|
|
|
@patch("vocalinux.ui.first_run_dialog.FirstRunDialog")
|
|
def test_show_first_run_dialog_returns_result_yes(self, mock_dialog_class):
|
|
"""Test that show_first_run_dialog returns 'yes'."""
|
|
from vocalinux.ui.first_run_dialog import show_first_run_dialog
|
|
|
|
mock_dialog = MagicMock()
|
|
mock_dialog.result = "yes"
|
|
mock_dialog_class.return_value = mock_dialog
|
|
|
|
result = show_first_run_dialog()
|
|
|
|
self.assertEqual(result, "yes")
|
|
|
|
@patch("vocalinux.ui.first_run_dialog.FirstRunDialog")
|
|
def test_show_first_run_dialog_returns_result_no(self, mock_dialog_class):
|
|
"""Test that show_first_run_dialog returns 'no'."""
|
|
from vocalinux.ui.first_run_dialog import show_first_run_dialog
|
|
|
|
mock_dialog = MagicMock()
|
|
mock_dialog.result = "no"
|
|
mock_dialog_class.return_value = mock_dialog
|
|
|
|
result = show_first_run_dialog()
|
|
|
|
self.assertEqual(result, "no")
|
|
|
|
@patch("vocalinux.ui.first_run_dialog.FirstRunDialog")
|
|
def test_show_first_run_dialog_returns_result_later(self, mock_dialog_class):
|
|
"""Test that show_first_run_dialog returns 'later'."""
|
|
from vocalinux.ui.first_run_dialog import show_first_run_dialog
|
|
|
|
mock_dialog = MagicMock()
|
|
mock_dialog.result = "later"
|
|
mock_dialog_class.return_value = mock_dialog
|
|
|
|
result = show_first_run_dialog()
|
|
|
|
self.assertEqual(result, "later")
|
|
|
|
@patch("vocalinux.ui.first_run_dialog.FirstRunDialog")
|
|
def test_show_first_run_dialog_returns_result_none(self, mock_dialog_class):
|
|
"""Test that show_first_run_dialog returns None."""
|
|
from vocalinux.ui.first_run_dialog import show_first_run_dialog
|
|
|
|
mock_dialog = MagicMock()
|
|
mock_dialog.result = None
|
|
mock_dialog_class.return_value = mock_dialog
|
|
|
|
result = show_first_run_dialog()
|
|
|
|
self.assertIsNone(result)
|
|
|
|
@patch("vocalinux.ui.first_run_dialog.FirstRunDialog")
|
|
def test_show_first_run_dialog_with_parent(self, mock_dialog_class):
|
|
"""Test that show_first_run_dialog accepts parent window."""
|
|
from vocalinux.ui.first_run_dialog import show_first_run_dialog
|
|
|
|
mock_dialog = MagicMock()
|
|
mock_dialog.result = "yes"
|
|
mock_dialog_class.return_value = mock_dialog
|
|
mock_parent = MagicMock()
|
|
|
|
result = show_first_run_dialog(parent=mock_parent)
|
|
|
|
# Dialog class should be called with parent argument
|
|
self.assertEqual(result, "yes")
|
|
self.assertTrue(mock_dialog_class.called)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|