Files
voculinux/tests/test_first_run_dialog_ext.py
T
Jatin K MalikandGitHub 8c4f743e82 fix(ui): first-run dialog response without Gtk.Dialog.do_response (#580)
* 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.
2026-07-24 00:44:47 -07:00

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()