diff --git a/tests/test_about_dialog.py b/tests/test_about_dialog.py new file mode 100644 index 0000000..50f7f46 --- /dev/null +++ b/tests/test_about_dialog.py @@ -0,0 +1,178 @@ +""" +Tests for the About Dialog module. + +Tests the show_about_dialog function and AboutDialog class using source code +inspection to verify expected behavior (since GTK mocking is complex across +Python versions). +""" + +import os +import unittest + + +def _get_source_code(): + """Read the about_dialog.py source code.""" + source_path = os.path.join( + os.path.dirname(__file__), + "..", + "src", + "vocalinux", + "ui", + "about_dialog.py", + ) + with open(source_path, "r") as f: + return f.read() + + +class TestShowAboutDialogFunction(unittest.TestCase): + """Test cases for show_about_dialog function structure.""" + + def setUp(self): + """Set up test fixtures.""" + self.source_code = _get_source_code() + + def test_show_about_dialog_function_exists(self): + """Test that show_about_dialog function is defined.""" + self.assertIn("def show_about_dialog(", self.source_code) + + def test_show_about_dialog_accepts_parent_parameter(self): + """Test that show_about_dialog accepts a parent parameter.""" + self.assertIn("def show_about_dialog(parent", self.source_code) + + def test_show_about_dialog_creates_about_dialog(self): + """Test that show_about_dialog creates a Gtk.AboutDialog.""" + self.assertIn("Gtk.AboutDialog()", self.source_code) + + def test_show_about_dialog_sets_program_name(self): + """Test that show_about_dialog sets the program name.""" + self.assertIn('set_program_name("Vocalinux")', self.source_code) + + def test_show_about_dialog_sets_version(self): + """Test that show_about_dialog sets the version.""" + self.assertIn("set_version(__version__)", self.source_code) + + def test_show_about_dialog_sets_copyright(self): + """Test that show_about_dialog sets the copyright.""" + self.assertIn("set_copyright(__copyright__)", self.source_code) + + def test_show_about_dialog_sets_website(self): + """Test that show_about_dialog sets the website URL.""" + self.assertIn("set_website(__url__)", self.source_code) + self.assertIn('set_website_label("Star on GitHub")', self.source_code) + + def test_show_about_dialog_sets_license_type(self): + """Test that show_about_dialog sets the license type.""" + self.assertIn("set_license_type(Gtk.License.GPL_3_0)", self.source_code) + + def test_show_about_dialog_sets_authors(self): + """Test that show_about_dialog sets authors.""" + self.assertIn("set_authors(", self.source_code) + self.assertIn("Jatin K Malik", self.source_code) + + def test_show_about_dialog_adds_credit_sections(self): + """Test that show_about_dialog adds credit sections.""" + self.assertIn("add_credit_section(", self.source_code) + self.assertIn("Contributors", self.source_code) + self.assertIn("Built With", self.source_code) + + def test_show_about_dialog_handles_logo(self): + """Test that show_about_dialog handles logo loading.""" + self.assertIn("get_icon_path", self.source_code) + self.assertIn("set_logo(", self.source_code) + self.assertIn("scale_simple(128, 128", self.source_code) + + def test_show_about_dialog_runs_and_destroys(self): + """Test that show_about_dialog runs and destroys the dialog.""" + self.assertIn("about_dialog.run()", self.source_code) + self.assertIn("about_dialog.destroy()", self.source_code) + + def test_show_about_dialog_sets_transient_for(self): + """Test that show_about_dialog sets transient parent.""" + self.assertIn("set_transient_for(parent)", self.source_code) + + def test_show_about_dialog_sets_modal(self): + """Test that show_about_dialog sets modal.""" + self.assertIn("set_modal(True)", self.source_code) + + +class TestAboutDialogClass(unittest.TestCase): + """Test cases for AboutDialog custom class.""" + + def setUp(self): + """Set up test fixtures.""" + self.source_code = _get_source_code() + + def test_about_dialog_class_exists(self): + """Test that AboutDialog class is defined.""" + self.assertIn("class AboutDialog(Gtk.Dialog):", self.source_code) + + def test_about_dialog_has_css(self): + """Test that AboutDialog has CSS styling defined.""" + self.assertIn("ABOUT_CSS", self.source_code) + self.assertIn(".about-dialog", self.source_code) + self.assertIn(".about-header", self.source_code) + + def test_about_dialog_has_setup_css_method(self): + """Test that AboutDialog has _setup_css method.""" + self.assertIn("def _setup_css(self)", self.source_code) + + def test_about_dialog_has_build_ui_method(self): + """Test that AboutDialog has _build_ui method.""" + self.assertIn("def _build_ui(self)", self.source_code) + + def test_about_dialog_has_build_header_method(self): + """Test that AboutDialog has _build_header method.""" + self.assertIn("def _build_header(self)", self.source_code) + + def test_about_dialog_has_build_links_method(self): + """Test that AboutDialog has _build_links method.""" + self.assertIn("def _build_links(self)", self.source_code) + + def test_about_dialog_has_build_credits_method(self): + """Test that AboutDialog has _build_credits method.""" + self.assertIn("def _build_credits(self)", self.source_code) + + def test_about_dialog_has_build_footer_method(self): + """Test that AboutDialog has _build_footer method.""" + self.assertIn("def _build_footer(self)", self.source_code) + + def test_about_dialog_uses_close_button(self): + """Test that AboutDialog uses Close-only button pattern.""" + self.assertIn("_Close", self.source_code) + + +class TestAboutDialogCSS(unittest.TestCase): + """Test cases for AboutDialog CSS styling.""" + + def setUp(self): + """Set up test fixtures.""" + self.source_code = _get_source_code() + + def test_css_has_about_icon_style(self): + """Test that CSS includes about-icon class.""" + self.assertIn(".about-icon", self.source_code) + + def test_css_has_about_title_style(self): + """Test that CSS includes about-title class.""" + self.assertIn(".about-title", self.source_code) + + def test_css_has_about_version_style(self): + """Test that CSS includes about-version class.""" + self.assertIn(".about-version", self.source_code) + + def test_css_has_about_description_style(self): + """Test that CSS includes about-description class.""" + self.assertIn(".about-description", self.source_code) + + def test_css_has_about_section_style(self): + """Test that CSS includes about-section class.""" + self.assertIn(".about-section", self.source_code) + + def test_css_uses_theme_variables(self): + """Test that CSS uses GTK theme variables.""" + self.assertIn("@theme_bg_color", self.source_code) + self.assertIn("@theme_base_color", self.source_code) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_logging_dialog.py b/tests/test_logging_dialog.py new file mode 100644 index 0000000..b338b61 --- /dev/null +++ b/tests/test_logging_dialog.py @@ -0,0 +1,298 @@ +""" +Tests for the Logging Dialog module. + +Tests the LoggingDialog class and its UI components using source code +inspection to verify expected behavior (since GTK mocking is complex across +Python versions). +""" + +import os +import unittest + + +def _get_source_code(): + """Read the logging_dialog.py source code.""" + source_path = os.path.join( + os.path.dirname(__file__), + "..", + "src", + "vocalinux", + "ui", + "logging_dialog.py", + ) + with open(source_path, "r") as f: + return f.read() + + +class TestLoggingDialogCSS(unittest.TestCase): + """Test cases for LoggingDialog CSS styling.""" + + def setUp(self): + """Set up test fixtures.""" + self.source_code = _get_source_code() + + def test_logging_css_exists(self): + """Test that LOGGING_CSS constant is defined.""" + self.assertIn("LOGGING_CSS", self.source_code) + + def test_logging_css_has_dialog_class(self): + """Test that CSS includes logging-dialog class.""" + self.assertIn(".logging-dialog", self.source_code) + + def test_logging_css_has_filter_bar(self): + """Test that CSS includes filter-bar class.""" + self.assertIn(".filter-bar", self.source_code) + + def test_logging_css_has_log_view(self): + """Test that CSS includes log-view class.""" + self.assertIn(".log-view", self.source_code) + + def test_logging_css_has_level_classes(self): + """Test that CSS includes level indicator classes.""" + self.assertIn(".level-debug", self.source_code) + self.assertIn(".level-info", self.source_code) + self.assertIn(".level-warning", self.source_code) + self.assertIn(".level-error", self.source_code) + self.assertIn(".level-critical", self.source_code) + + def test_logging_css_has_status_bar(self): + """Test that CSS includes status-bar class.""" + self.assertIn(".status-bar", self.source_code) + + def test_logging_css_uses_theme_variables(self): + """Test that CSS uses GTK theme variables for theming.""" + # Should use theme variables for proper light/dark mode support + self.assertIn("@theme_bg_color", self.source_code) + self.assertIn("@theme_base_color", self.source_code) + self.assertIn("@theme_fg_color", self.source_code) + + +class TestLoggingDialogClass(unittest.TestCase): + """Test cases for LoggingDialog class structure.""" + + def setUp(self): + """Set up test fixtures.""" + self.source_code = _get_source_code() + + def test_logging_dialog_class_exists(self): + """Test that LoggingDialog class is defined.""" + self.assertIn("class LoggingDialog(Gtk.Dialog):", self.source_code) + + def test_dialog_title(self): + """Test that dialog has correct title.""" + self.assertIn('title="Logs"', self.source_code) + + def test_dialog_uses_close_button(self): + """Test that dialog uses Close-only button pattern.""" + self.assertIn("_Close", self.source_code) + + def test_dialog_registers_callback(self): + """Test that dialog registers callback with logging manager.""" + self.assertIn("register_callback(self._on_new_log_record)", self.source_code) + + def test_dialog_unregisters_callback(self): + """Test that dialog unregisters callback on destroy.""" + self.assertIn("unregister_callback(self._on_new_log_record)", self.source_code) + + +class TestLoggingDialogFilterBar(unittest.TestCase): + """Test cases for LoggingDialog filter bar.""" + + def setUp(self): + """Set up test fixtures.""" + self.source_code = _get_source_code() + + def test_create_filter_bar_method_exists(self): + """Test that _create_filter_bar method exists.""" + self.assertIn("def _create_filter_bar(self)", self.source_code) + + def test_filter_bar_has_level_filter(self): + """Test that filter bar has level filter combo box.""" + # Check for level filter options + self.assertIn('"All Levels"', self.source_code) + self.assertIn('"Debug"', self.source_code) + self.assertIn('"Info"', self.source_code) + self.assertIn('"Warning"', self.source_code) + self.assertIn('"Error"', self.source_code) + self.assertIn('"Critical"', self.source_code) + + def test_filter_bar_has_module_filter(self): + """Test that filter bar has module filter entry.""" + self.assertIn("module_entry", self.source_code) + self.assertIn("Filter by module name", self.source_code) + + def test_filter_bar_has_auto_scroll(self): + """Test that filter bar has auto-scroll checkbox.""" + self.assertIn("auto_scroll_check", self.source_code) + self.assertIn("Auto-scroll", self.source_code) + + +class TestLoggingDialogActions(unittest.TestCase): + """Test cases for LoggingDialog action buttons.""" + + def setUp(self): + """Set up test fixtures.""" + self.source_code = _get_source_code() + + def test_export_logs_method_exists(self): + """Test that _export_logs method exists.""" + self.assertIn("def _export_logs(self)", self.source_code) + + def test_copy_logs_method_exists(self): + """Test that _copy_logs_to_clipboard method exists.""" + self.assertIn("def _copy_logs_to_clipboard(self)", self.source_code) + + def test_clear_logs_method_exists(self): + """Test that _clear_logs method exists.""" + self.assertIn("def _clear_logs(self)", self.source_code) + + def test_refresh_logs_method_exists(self): + """Test that _refresh_logs method exists.""" + self.assertIn("def _refresh_logs(self)", self.source_code) + + def test_toolbar_has_refresh_button(self): + """Test that toolbar has refresh button.""" + self.assertIn("Refresh", self.source_code) + self.assertIn("view-refresh-symbolic", self.source_code) + + def test_toolbar_has_copy_button(self): + """Test that toolbar has copy button.""" + self.assertIn("Copy", self.source_code) + self.assertIn("edit-copy-symbolic", self.source_code) + + def test_toolbar_has_export_button(self): + """Test that toolbar has export button.""" + self.assertIn("Export", self.source_code) + self.assertIn("document-save-symbolic", self.source_code) + + def test_toolbar_has_clear_button(self): + """Test that toolbar has clear button.""" + self.assertIn("Clear", self.source_code) + self.assertIn("user-trash-symbolic", self.source_code) + + +class TestLoggingDialogTextTags(unittest.TestCase): + """Test cases for LoggingDialog text buffer tags.""" + + def setUp(self): + """Set up test fixtures.""" + self.source_code = _get_source_code() + + def test_create_text_tags_method_exists(self): + """Test that _create_text_tags method exists.""" + self.assertIn("def _create_text_tags(self)", self.source_code) + + def test_log_levels_have_tags(self): + """Test that all log levels have text tags defined.""" + self.assertIn('create_tag("DEBUG")', self.source_code) + self.assertIn('create_tag("INFO")', self.source_code) + self.assertIn('create_tag("WARNING")', self.source_code) + self.assertIn('create_tag("ERROR")', self.source_code) + self.assertIn('create_tag("CRITICAL")', self.source_code) + + +class TestLoggingDialogStatusBar(unittest.TestCase): + """Test cases for LoggingDialog status bar.""" + + def setUp(self): + """Set up test fixtures.""" + self.source_code = _get_source_code() + + def test_create_status_bar_method_exists(self): + """Test that _create_status_bar method exists.""" + self.assertIn("def _create_status_bar(self)", self.source_code) + + def test_update_status_method_exists(self): + """Test that _update_status method exists.""" + self.assertIn("def _update_status(self)", self.source_code) + + def test_status_bar_shows_total_count(self): + """Test that status bar shows total record count.""" + self.assertIn("total_label", self.source_code) + self.assertIn("records", self.source_code) + + +class TestLoggingDialogAutoScroll(unittest.TestCase): + """Test cases for LoggingDialog auto-scroll functionality.""" + + def setUp(self): + """Set up test fixtures.""" + self.source_code = _get_source_code() + + def test_scroll_to_bottom_method_exists(self): + """Test that _scroll_to_bottom method exists.""" + self.assertIn("def _scroll_to_bottom(self)", self.source_code) + + def test_on_auto_scroll_toggled_method_exists(self): + """Test that _on_auto_scroll_toggled method exists.""" + self.assertIn("def _on_auto_scroll_toggled(self", self.source_code) + + def test_auto_scroll_enabled_by_default(self): + """Test that auto-scroll is enabled by default.""" + self.assertIn("self.auto_scroll = True", self.source_code) + + +class TestLoggingDialogToast(unittest.TestCase): + """Test cases for LoggingDialog toast notifications.""" + + def setUp(self): + """Set up test fixtures.""" + self.source_code = _get_source_code() + + def test_show_toast_method_exists(self): + """Test that _show_toast method exists.""" + self.assertIn("def _show_toast(self, message: str)", self.source_code) + + def test_show_message_method_exists(self): + """Test that _show_message method exists.""" + self.assertIn("def _show_message(", self.source_code) + + def test_toast_uses_revealer(self): + """Test that toast uses Gtk.Revealer for animation.""" + self.assertIn("Gtk.Revealer()", self.source_code) + self.assertIn("SLIDE_DOWN", self.source_code) + + +class TestLoggingDialogLogView(unittest.TestCase): + """Test cases for LoggingDialog log view area.""" + + def setUp(self): + """Set up test fixtures.""" + self.source_code = _get_source_code() + + def test_create_log_view_method_exists(self): + """Test that _create_log_view method exists.""" + self.assertIn("def _create_log_view(self)", self.source_code) + + def test_log_view_is_not_editable(self): + """Test that log view text is not editable.""" + self.assertIn("set_editable(False)", self.source_code) + + def test_log_view_has_word_wrap(self): + """Test that log view has word wrap enabled.""" + self.assertIn("WORD_CHAR", self.source_code) + + def test_append_log_record_method_exists(self): + """Test that _append_log_record method exists.""" + self.assertIn("def _append_log_record(self, record", self.source_code) + + +class TestSetupCSSFunction(unittest.TestCase): + """Test cases for _setup_css function.""" + + def setUp(self): + """Set up test fixtures.""" + self.source_code = _get_source_code() + + def test_setup_css_function_exists(self): + """Test that _setup_css function is defined.""" + self.assertIn("def _setup_css():", self.source_code) + + def test_setup_css_creates_provider(self): + """Test that _setup_css creates a CSS provider.""" + self.assertIn("Gtk.CssProvider()", self.source_code) + self.assertIn("load_from_data(LOGGING_CSS.encode())", self.source_code) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_settings_dialog.py b/tests/test_settings_dialog.py index a070bba..f4376c8 100644 --- a/tests/test_settings_dialog.py +++ b/tests/test_settings_dialog.py @@ -4,18 +4,24 @@ Tests for the SettingsDialog. Since SettingsDialog inherits from Gtk.Dialog which is mocked during tests, we can't easily test the actual class methods. Instead, we test the core logic that the settings dialog is supposed to execute. + +UX Design Notes tested: +- Instant-apply pattern: settings apply immediately when changed +- Close-only button (no Apply/Cancel) consistent with GNOME Settings """ import sys import time import unittest -from unittest.mock import MagicMock, Mock +from unittest.mock import MagicMock, Mock, patch # Mock GTK before importing anything that might use it sys.modules["gi"] = MagicMock() sys.modules["gi.repository"] = MagicMock() sys.modules["gi.repository.Gtk"] = MagicMock() sys.modules["gi.repository.GLib"] = MagicMock() +sys.modules["gi.repository.Gdk"] = MagicMock() +sys.modules["gi.repository.Pango"] = MagicMock() from vocalinux.common_types import RecognitionState # noqa: E402 @@ -154,5 +160,196 @@ class TestSettingsDialog(unittest.TestCase): mock_speech_engine.reconfigure.assert_called_once() +class TestSettingsDialogCSS(unittest.TestCase): + """Test cases for SettingsDialog CSS styling.""" + + def setUp(self): + """Set up test fixtures.""" + # Clear any cached imports + if "vocalinux.ui.settings_dialog" in sys.modules: + del sys.modules["vocalinux.ui.settings_dialog"] + + def test_settings_css_exists(self): + """Test that SETTINGS_CSS constant is defined.""" + from vocalinux.ui.settings_dialog import SETTINGS_CSS + + self.assertIsInstance(SETTINGS_CSS, str) + + def test_settings_css_has_dialog_class(self): + """Test that CSS includes settings-dialog class.""" + from vocalinux.ui.settings_dialog import SETTINGS_CSS + + self.assertIn(".settings-dialog", SETTINGS_CSS) + + def test_settings_css_has_preferences_group(self): + """Test that CSS includes preferences-group class.""" + from vocalinux.ui.settings_dialog import SETTINGS_CSS + + self.assertIn(".preferences-group", SETTINGS_CSS) + + def test_settings_css_has_preference_row(self): + """Test that CSS includes preference-row class.""" + from vocalinux.ui.settings_dialog import SETTINGS_CSS + + self.assertIn(".preference-row", SETTINGS_CSS) + + def test_settings_css_uses_theme_variables(self): + """Test that CSS uses GTK theme variables.""" + from vocalinux.ui.settings_dialog import SETTINGS_CSS + + # Should use theme variables for proper light/dark mode support + self.assertIn("@theme_bg_color", SETTINGS_CSS) + self.assertIn("@theme_base_color", SETTINGS_CSS) + + def test_settings_css_has_status_classes(self): + """Test that CSS includes status indicator classes.""" + from vocalinux.ui.settings_dialog import SETTINGS_CSS + + self.assertIn(".status-success", SETTINGS_CSS) + self.assertIn(".status-warning", SETTINGS_CSS) + self.assertIn(".status-error", SETTINGS_CSS) + + +class TestSettingsDialogClasses(unittest.TestCase): + """Test cases for SettingsDialog helper classes.""" + + def setUp(self): + """Set up test fixtures.""" + if "vocalinux.ui.settings_dialog" in sys.modules: + del sys.modules["vocalinux.ui.settings_dialog"] + + def test_preferences_group_class_exists(self): + """Test that PreferencesGroup class exists.""" + from vocalinux.ui.settings_dialog import PreferencesGroup + + self.assertTrue(callable(PreferencesGroup)) + + def test_preference_row_class_exists(self): + """Test that PreferenceRow class exists.""" + from vocalinux.ui.settings_dialog import PreferenceRow + + self.assertTrue(callable(PreferenceRow)) + + def test_model_download_dialog_class_exists(self): + """Test that ModelDownloadDialog class exists.""" + from vocalinux.ui.settings_dialog import ModelDownloadDialog + + self.assertTrue(callable(ModelDownloadDialog)) + + +class TestSettingsDialogInstantApply(unittest.TestCase): + """Test cases for instant-apply behavior (Close-only button pattern).""" + + def setUp(self): + """Set up test fixtures.""" + if "vocalinux.ui.settings_dialog" in sys.modules: + del sys.modules["vocalinux.ui.settings_dialog"] + + def test_settings_dialog_has_auto_apply_method(self): + """Test that SettingsDialog has _auto_apply_settings method in source.""" + import os + + source_path = os.path.join( + os.path.dirname(__file__), + "..", + "src", + "vocalinux", + "ui", + "settings_dialog.py", + ) + with open(source_path, "r") as f: + source_code = f.read() + + self.assertIn("def _auto_apply_settings(self", source_code) + + def test_settings_dialog_close_only_pattern(self): + """Test that SettingsDialog uses Close-only button pattern (no Apply/Cancel).""" + import os + + source_path = os.path.join( + os.path.dirname(__file__), + "..", + "src", + "vocalinux", + "ui", + "settings_dialog.py", + ) + with open(source_path, "r") as f: + source_code = f.read() + + # Check for Close button + self.assertIn("_Close", source_code) + # Should NOT have Apply or Cancel buttons (instant-apply pattern) + self.assertNotIn("_Apply", source_code) + self.assertNotIn("ResponseType.APPLY", source_code) + + def test_settings_dialog_no_revert_settings(self): + """Test that SettingsDialog does NOT have _revert_settings (removed).""" + from vocalinux.ui.settings_dialog import SettingsDialog + + # _revert_settings was removed as part of Close-only button pattern + self.assertFalse(hasattr(SettingsDialog, "_revert_settings")) + + def test_settings_dialog_no_show_applied_message(self): + """Test that SettingsDialog does NOT have _show_settings_applied_message (removed).""" + from vocalinux.ui.settings_dialog import SettingsDialog + + # _show_settings_applied_message was removed as part of instant-apply pattern + self.assertFalse(hasattr(SettingsDialog, "_show_settings_applied_message")) + + +class TestSettingsDialogHelperFunctions(unittest.TestCase): + """Test cases for settings dialog helper functions.""" + + def setUp(self): + """Set up test fixtures.""" + if "vocalinux.ui.settings_dialog" in sys.modules: + del sys.modules["vocalinux.ui.settings_dialog"] + + def test_format_size_function_exists(self): + """Test that _format_size function exists.""" + from vocalinux.ui.settings_dialog import _format_size + + self.assertTrue(callable(_format_size)) + + def test_format_size_mb(self): + """Test _format_size with MB values.""" + from vocalinux.ui.settings_dialog import _format_size + + self.assertEqual(_format_size(100), "100 MB") + self.assertEqual(_format_size(500), "500 MB") + + def test_format_size_gb(self): + """Test _format_size with GB values.""" + from vocalinux.ui.settings_dialog import _format_size + + self.assertEqual(_format_size(1000), "1.0 GB") + self.assertEqual(_format_size(2500), "2.5 GB") + + def test_is_whisper_model_downloaded_function_exists(self): + """Test that _is_whisper_model_downloaded function exists.""" + from vocalinux.ui.settings_dialog import _is_whisper_model_downloaded + + self.assertTrue(callable(_is_whisper_model_downloaded)) + + def test_is_vosk_model_downloaded_function_exists(self): + """Test that _is_vosk_model_downloaded function exists.""" + from vocalinux.ui.settings_dialog import _is_vosk_model_downloaded + + self.assertTrue(callable(_is_vosk_model_downloaded)) + + def test_get_recommended_whisper_model_function_exists(self): + """Test that _get_recommended_whisper_model function exists.""" + from vocalinux.ui.settings_dialog import _get_recommended_whisper_model + + self.assertTrue(callable(_get_recommended_whisper_model)) + + def test_get_recommended_vosk_model_function_exists(self): + """Test that _get_recommended_vosk_model function exists.""" + from vocalinux.ui.settings_dialog import _get_recommended_vosk_model + + self.assertTrue(callable(_get_recommended_vosk_model)) + + if __name__ == "__main__": unittest.main()