# Unit tests for speech_presenter.py methods. # # Copyright 2025 Igalia, S.L. # Author: Joanmarie Diggs # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # pylint: disable=wrong-import-position # pylint: disable=import-outside-toplevel # pylint: disable=too-many-public-methods # pylint: disable=too-many-statements # pylint: disable=too-many-lines # pylint: disable=protected-access # pylint: disable=too-many-arguments # pylint: disable=too-many-positional-arguments # pylint: disable=too-many-locals """Unit tests for speech_presenter.py methods.""" from __future__ import annotations from typing import TYPE_CHECKING import pytest if TYPE_CHECKING: from unittest.mock import MagicMock from cthulhu_test_context import CthulhuTestContext @pytest.mark.unit class TestSpeechPresenter: """Test SpeechPresenter class methods.""" def _setup_dependencies(self, test_context: CthulhuTestContext) -> dict[str, MagicMock]: """Set up mocks for speech_presenter dependencies.""" additional_modules = [ "cthulhu.document_presenter", "cthulhu.mathsymbols", "cthulhu.object_properties", "cthulhu.phonnames", "cthulhu.pronunciation_dictionary_manager", "cthulhu.ax_hypertext", "cthulhu.ax_table", "cthulhu.ax_text", "cthulhu.ax_utilities", "cthulhu.ax_document", "cthulhu.presentation_manager", "cthulhu.preferences_grid_base", "cthulhu.speech", "cthulhu.speech_manager", "cthulhu.speech_monitor", ] essential_modules = test_context.setup_shared_dependencies(additional_modules) document_presenter_mock = essential_modules["cthulhu.document_presenter"] document_presenter_instance = test_context.Mock() document_presenter_instance.get_in_focus_mode = test_context.Mock(return_value=False) document_presenter_mock.get_presenter = test_context.Mock( return_value=document_presenter_instance, ) dbus_service_mock = essential_modules["cthulhu.dbus_service"] dbus_service_mock.get_remote_controller.return_value = test_context.Mock() def passthrough_decorator(func): return func dbus_service_mock.getter = passthrough_decorator dbus_service_mock.setter = passthrough_decorator dbus_service_mock.command = passthrough_decorator dbus_service_mock.parameterized_command = passthrough_decorator debug_mock = essential_modules["cthulhu.debug"] debug_mock.print_message.return_value = None debug_mock.print_tokens.return_value = None debug_mock.LEVEL_INFO = 800 debug_mock.LEVEL_WARNING = 900 ax_hypertext_mock = essential_modules["cthulhu.ax_hypertext"] ax_hypertext_mock.AXHypertext = test_context.Mock() ax_hypertext_mock.AXHypertext.get_all_links_in_range = test_context.Mock(return_value=[]) ax_hypertext_mock.AXHypertext.get_link_end_offset = test_context.Mock(return_value=0) ax_object_mock = essential_modules["cthulhu.ax_object"] ax_object_mock.AXObject = test_context.Mock() ax_text_mock = essential_modules["cthulhu.ax_text"] ax_text_mock.AXText = test_context.Mock() ax_text_mock.AXText.get_character_at_offset = test_context.Mock(return_value=("a", 0)) ax_utilities_mock = essential_modules["cthulhu.ax_utilities"] ax_utilities_mock.AXUtilities = test_context.Mock() ax_utilities_mock.AXUtilities.find_ancestor_inclusive = test_context.Mock(return_value=None) ax_utilities_mock.AXUtilities.string_has_spelling_error = test_context.Mock( return_value=False, ) ax_utilities_mock.AXUtilities.string_has_grammar_error = test_context.Mock( return_value=False, ) ax_utilities_mock.AXUtilities.get_table = test_context.Mock(return_value=None) ax_utilities_mock.AXUtilities.is_math_related = test_context.Mock(return_value=False) ax_utilities_mock.AXUtilities.is_text_input_telephone = test_context.Mock( return_value=False, ) ax_utilities_mock.AXUtilities.is_code = test_context.Mock(return_value=False) ax_document_mock = essential_modules["cthulhu.ax_document"] ax_document_mock.AXDocument = test_context.Mock() ax_document_mock.AXDocument.is_plain_text = test_context.Mock(return_value=False) mathsymbols_mock = essential_modules["cthulhu.mathsymbols"] mathsymbols_mock.adjust_for_speech = test_context.Mock(side_effect=lambda x: x) object_properties_mock = essential_modules["cthulhu.object_properties"] object_properties_mock.STATE_INVALID_GRAMMAR_SPEECH = "grammar error" pronunciation_dict_mock = essential_modules["cthulhu.pronunciation_dictionary_manager"] pron_manager_instance = test_context.Mock() pron_manager_instance.get_pronunciation = test_context.Mock(side_effect=lambda x: x) pronunciation_dict_mock.get_manager = test_context.Mock(return_value=pron_manager_instance) from cthulhu import gsettings_registry gsettings_registry.get_registry().clear_runtime_values() cthulhu_i18n_mock = essential_modules["cthulhu.cthulhu_i18n"] cthulhu_i18n_mock._ = lambda x: x cthulhu_i18n_mock.C_ = lambda c, x: x cthulhu_i18n_mock.ngettext = lambda s, p, n: s if n == 1 else p messages_mock = essential_modules["cthulhu.messages"] messages_mock.LINK = "link" messages_mock.MISSPELLED = "misspelled" messages_mock.repeated_char_count = test_context.Mock( side_effect=lambda char, count: f"{char} repeated {count} times", ) messages_mock.spaces_count = test_context.Mock(side_effect=lambda count: f"{count} spaces") messages_mock.tabs_count = test_context.Mock(side_effect=lambda count: f"{count} tabs") return essential_modules def test_init(self, test_context: CthulhuTestContext) -> None: """Test presenter initialization and D-Bus registration.""" essential_modules: dict[str, MagicMock] = self._setup_dependencies(test_context) from cthulhu.speech_presenter import SpeechPresenter presenter = SpeechPresenter() # Verify D-Bus registration occurred controller = essential_modules["cthulhu.dbus_service"].get_remote_controller() controller.register_decorated_module.assert_called_with("SpeechPresenter", presenter) # Monitor callbacks are not registered in __init__ (deferred to set_up_commands # to avoid circular imports during module loading). def test_set_up_commands(self, test_context: CthulhuTestContext) -> None: """Test that set_up_commands registers commands in CommandManager.""" self._setup_dependencies(test_context) from cthulhu import command_manager from cthulhu.speech_presenter import SpeechPresenter presenter = SpeechPresenter() presenter.set_up_commands() # Verify commands are registered in CommandManager cmd_manager = command_manager.get_manager() assert cmd_manager.get_command("changeNumberStyleHandler") is not None assert cmd_manager.get_command("toggleSpeechVerbosityHandler") is not None assert cmd_manager.get_command("toggleSpeakingIndentationJustificationHandler") is not None assert cmd_manager.get_command("toggleTableCellReadModeHandler") is not None def test_set_up_commands_registers_monitor_callbacks( self, test_context: CthulhuTestContext, ) -> None: """Test that set_up_commands registers speech monitor callbacks.""" essential_modules: dict[str, MagicMock] = self._setup_dependencies(test_context) from cthulhu.speech_presenter import SpeechPresenter presenter = SpeechPresenter() speech_mock = essential_modules["cthulhu.speech"] speech_mock.set_monitor_callbacks.reset_mock() presenter.set_up_commands() speech_mock.set_monitor_callbacks.assert_called_once_with( write_text=presenter.write_to_monitor, write_key=presenter.write_key_to_monitor, begin_group=presenter._begin_monitor_group, end_group=presenter._end_monitor_group, ) @pytest.mark.parametrize( "case", [ {"id": "verbose_true", "setting_value": 1, "expected": True}, {"id": "verbose_false", "setting_value": 0, "expected": False}, ], ids=lambda case: case["id"], ) def test_use_verbose_speech(self, test_context: CthulhuTestContext, case: dict) -> None: """Test use_verbose_speech method.""" self._setup_dependencies(test_context) from cthulhu import gsettings_registry from cthulhu.speech_presenter import SpeechPresenter presenter = SpeechPresenter() nick = "verbose" if case["setting_value"] == 1 else "brief" gsettings_registry.get_registry().set_runtime_value("speech", "verbosity-level", nick) result = presenter.use_verbose_speech() assert result == case["expected"] @pytest.mark.parametrize( "case", [ {"id": "verbosity_brief", "setting_value": 0, "expected": "brief"}, {"id": "verbosity_verbose", "setting_value": 1, "expected": "verbose"}, ], ids=lambda case: case["id"], ) def test_get_verbosity_level(self, test_context: CthulhuTestContext, case: dict) -> None: """Test get_verbosity_level method.""" self._setup_dependencies(test_context) from cthulhu import gsettings_registry from cthulhu.speech_presenter import SpeechPresenter presenter = SpeechPresenter() nick = "verbose" if case["setting_value"] == 1 else "brief" gsettings_registry.get_registry().set_runtime_value("speech", "verbosity-level", nick) result = presenter.get_verbosity_level() assert result == case["expected"] @pytest.mark.parametrize( "case", [ { "id": "set_verbosity_brief", "input_value": "brief", "expected": True, }, { "id": "set_verbosity_verbose", "input_value": "verbose", "expected": True, }, { "id": "set_verbosity_invalid", "input_value": "invalid", "expected": False, }, ], ids=lambda case: case["id"], ) def test_set_verbosity_level(self, test_context: CthulhuTestContext, case: dict) -> None: """Test set_verbosity_level method.""" self._setup_dependencies(test_context) from cthulhu.speech_presenter import SpeechPresenter presenter = SpeechPresenter() result = presenter.set_verbosity_level(case["input_value"]) assert result == case["expected"] if case["expected"]: assert presenter.get_verbosity_level() == case["input_value"] @pytest.mark.parametrize( "case", [ {"id": "speak_blank_lines_true", "setting_value": True, "expected": True}, {"id": "speak_blank_lines_false", "setting_value": False, "expected": False}, ], ids=lambda case: case["id"], ) def test_get_speak_blank_lines(self, test_context: CthulhuTestContext, case: dict) -> None: """Test get_speak_blank_lines method.""" self._setup_dependencies(test_context) from cthulhu import gsettings_registry from cthulhu.speech_presenter import SpeechPresenter presenter = SpeechPresenter() gsettings_registry.get_registry().set_runtime_value( "speech", "speak-blank-lines", case["setting_value"], ) result = presenter.get_speak_blank_lines() assert result == case["expected"] @pytest.mark.parametrize( "case", [ {"id": "only_speak_displayed_true", "setting_value": True, "expected": True}, {"id": "only_speak_displayed_false", "setting_value": False, "expected": False}, ], ids=lambda case: case["id"], ) def test_get_only_speak_displayed_text(self, test_context: CthulhuTestContext, case: dict) -> None: """Test get_only_speak_displayed_text method.""" self._setup_dependencies(test_context) from cthulhu import gsettings_registry from cthulhu.speech_presenter import SpeechPresenter presenter = SpeechPresenter() gsettings_registry.get_registry().set_runtime_value( "speech", "only-speak-displayed-text", case["setting_value"], ) result = presenter.get_only_speak_displayed_text() assert result == case["expected"] @pytest.mark.parametrize( "case", [ {"id": "set_only_speak_displayed_true", "input_value": True, "expected": True}, {"id": "set_only_speak_displayed_false", "input_value": False, "expected": True}, ], ids=lambda case: case["id"], ) def test_set_only_speak_displayed_text(self, test_context: CthulhuTestContext, case: dict) -> None: """Test set_only_speak_displayed_text method.""" self._setup_dependencies(test_context) from cthulhu.speech_presenter import SpeechPresenter presenter = SpeechPresenter() result = presenter.set_only_speak_displayed_text(case["input_value"]) assert result == case["expected"] assert presenter.get_only_speak_displayed_text() == case["input_value"] @pytest.mark.parametrize( "case", [ {"id": "speak_indentation_true", "setting_value": True, "expected": True}, {"id": "speak_indentation_false", "setting_value": False, "expected": False}, ], ids=lambda case: case["id"], ) def test_get_speak_indentation_and_justification( self, test_context: CthulhuTestContext, case: dict, ) -> None: """Test get_speak_indentation_and_justification method.""" self._setup_dependencies(test_context) from cthulhu import gsettings_registry from cthulhu.speech_presenter import SpeechPresenter presenter = SpeechPresenter() gsettings_registry.get_registry().set_runtime_value( "speech", "speak-indentation-and-justification", case["setting_value"], ) result = presenter.get_speak_indentation_and_justification() assert result == case["expected"] @pytest.mark.parametrize( "case", [ {"id": "set_speak_indentation_true", "input_value": True, "expected": True}, {"id": "set_speak_indentation_false", "input_value": False, "expected": True}, ], ids=lambda case: case["id"], ) def test_set_speak_indentation_and_justification( self, test_context: CthulhuTestContext, case: dict, ) -> None: """Test set_speak_indentation_and_justification method.""" self._setup_dependencies(test_context) from cthulhu.speech_presenter import SpeechPresenter presenter = SpeechPresenter() result = presenter.set_speak_indentation_and_justification(case["input_value"]) assert result == case["expected"] assert presenter.get_speak_indentation_and_justification() == case["input_value"] def test_get_indentation_description_disabled(self, test_context: CthulhuTestContext) -> None: """Test get_indentation_description method when disabled.""" self._setup_dependencies(test_context) from cthulhu import gsettings_registry from cthulhu.speech_presenter import SpeechPresenter registry = gsettings_registry.get_registry() registry.set_runtime_value("speech", "only-speak-displayed-text", True) registry.set_runtime_value("speech", "speak-indentation-and-justification", False) presenter = SpeechPresenter() line = " Hello world" result = presenter.get_indentation_description(line) assert result == "" def test_get_indentation_description_enabled(self, test_context: CthulhuTestContext) -> None: """Test get_indentation_description method when enabled.""" self._setup_dependencies(test_context) from cthulhu import gsettings_registry from cthulhu.speech_presenter import SpeechPresenter registry = gsettings_registry.get_registry() registry.set_runtime_value("speech", "only-speak-displayed-text", False) registry.set_runtime_value("speech", "speak-indentation-and-justification", True) registry.set_runtime_value("speech", "speak-indentation-only-if-changed", False) presenter = SpeechPresenter() line = " Hello world" result = presenter.get_indentation_description(line) assert result != "" def test_get_indentation_description_only_if_changed( self, test_context: CthulhuTestContext, ) -> None: """Test get_indentation_description with only-if-changed enabled.""" self._setup_dependencies(test_context) from cthulhu import gsettings_registry from cthulhu.speech_presenter import SpeechPresenter registry = gsettings_registry.get_registry() registry.set_runtime_value("speech", "only-speak-displayed-text", False) registry.set_runtime_value("speech", "speak-indentation-and-justification", True) registry.set_runtime_value("speech", "speak-indentation-only-if-changed", True) presenter = SpeechPresenter() line = " Hello world" # First call should return a description result1 = presenter.get_indentation_description(line) assert result1 != "" # Second call with same indentation should return empty result2 = presenter.get_indentation_description(line) assert result2 == "" def test_get_error_description_basic(self, test_context: CthulhuTestContext) -> None: """Test get_error_description method with basic scenarios.""" essential_modules: dict[str, MagicMock] = self._setup_dependencies(test_context) ax_text_mock = essential_modules["cthulhu.ax_text"] ax_text_mock.AXText.get_character_at_offset = test_context.Mock(return_value=("a", 0)) ax_utilities_mock = essential_modules["cthulhu.ax_utilities"] ax_utilities_mock.AXUtilities.string_has_spelling_error = test_context.Mock( return_value=True, ) ax_utilities_mock.AXUtilities.string_has_grammar_error = test_context.Mock( return_value=False, ) from cthulhu.speech_presenter import SpeechPresenter presenter = SpeechPresenter() mock_obj = test_context.Mock() result = presenter.get_error_description(mock_obj, 0) assert result == "misspelled" def test_get_error_description_disabled(self, test_context: CthulhuTestContext) -> None: """Test get_error_description method when misspelled indicator is disabled.""" self._setup_dependencies(test_context) from cthulhu import gsettings_registry from cthulhu.speech_presenter import SpeechPresenter gsettings_registry.get_registry().set_runtime_value( "speech", "speak-misspelled-indicator", False, ) presenter = SpeechPresenter() mock_obj = test_context.Mock() result = presenter.get_error_description(mock_obj, 0) assert result == "" def test_adjust_for_presentation(self, test_context: CthulhuTestContext) -> None: """Test adjust_for_presentation with all sub-adjustments mocked.""" self._setup_dependencies(test_context) from cthulhu.speech_presenter import SpeechPresenter presenter = SpeechPresenter() return_text = "Hello world" test_context.patch_object(presenter, "_adjust_for_links", return_value=return_text) test_context.patch_object(presenter, "adjust_for_digits", return_value=return_text) test_context.patch_object(presenter, "_adjust_for_repeats", return_value=return_text) test_context.patch_object( presenter, "_adjust_for_verbalized_punctuation", return_value=return_text, ) test_context.patch_object( presenter, "_apply_pronunciation_dictionary", return_value=return_text, ) mock_obj = test_context.Mock() result = presenter.adjust_for_presentation(mock_obj, "Hello world", start_offset=0) assert result == return_text @pytest.mark.parametrize( "case", [ { "id": "digits_on", "speak_digits": True, "is_telephone": False, "input_text": "123 Main", "expected": "1 2 3 Main", }, { "id": "digits_off", "speak_digits": False, "is_telephone": False, "input_text": "123 Main", "expected": "123 Main", }, { "id": "telephone_field", "speak_digits": False, "is_telephone": True, "input_text": "555", "expected": "5 5 5", }, ], ids=lambda case: case["id"], ) def test_adjust_for_digits(self, test_context: CthulhuTestContext, case: dict) -> None: """Test adjust_for_digits method with speakNumbersAsDigits on/off.""" essential_modules: dict[str, MagicMock] = self._setup_dependencies(test_context) from cthulhu import gsettings_registry gsettings_registry.get_registry().set_runtime_value( "speech", "speak-numbers-as-digits", case["speak_digits"], ) essential_modules[ "cthulhu.ax_utilities" ].AXUtilities.is_text_input_telephone = test_context.Mock(return_value=case["is_telephone"]) from cthulhu.speech_presenter import SpeechPresenter mock_obj = test_context.Mock() result = SpeechPresenter.adjust_for_digits(mock_obj, case["input_text"]) assert result == case["expected"] def test_adjust_for_repeats(self, test_context: CthulhuTestContext) -> None: """Test _adjust_for_repeats with repeated characters.""" self._setup_dependencies(test_context) from cthulhu.speech_presenter import SpeechPresenter presenter = SpeechPresenter() text = "----" result = presenter._adjust_for_repeats(text) # Should describe repeated characters assert "repeated" in result or result != text def test_adjust_for_repeats_short_text(self, test_context: CthulhuTestContext) -> None: """Test _adjust_for_repeats with text shorter than limit.""" self._setup_dependencies(test_context) from cthulhu.speech_presenter import SpeechPresenter presenter = SpeechPresenter() text = "hi" result = presenter._adjust_for_repeats(text) # Short text should be returned unchanged assert result == text def test_get_speech_preferences(self, test_context: CthulhuTestContext) -> None: """Test get_speech_preferences returns correct tuple structure.""" self._setup_dependencies(test_context) from cthulhu.speech_presenter import SpeechPresenter presenter = SpeechPresenter() result = presenter.get_speech_preferences() assert isinstance(result, tuple) assert len(result) == 3 general, object_details, announcements = result # general should have 1 preference assert len(general) == 1 assert general[0].prefs_key == "messages-are-detailed" # object_details should have 5 preferences assert len(object_details) == 5 assert object_details[0].prefs_key == "only-speak-displayed-text" # announcements should have 6 preferences assert len(announcements) == 6 assert announcements[0].prefs_key == "announce-blockquote" def test_apply_speech_preferences(self, test_context: CthulhuTestContext) -> None: """Test apply_speech_preferences applies values correctly.""" self._setup_dependencies(test_context) from cthulhu.speech_presenter import SpeechPreference, SpeechPresenter presenter = SpeechPresenter() mock_setter1 = test_context.Mock(return_value=True) mock_setter2 = test_context.Mock(return_value=True) pref1 = SpeechPreference("key1", "Label 1", lambda: True, mock_setter1) pref2 = SpeechPreference("key2", "Label 2", lambda: False, mock_setter2) updates = [(pref1, False), (pref2, True)] result = presenter.apply_speech_preferences(updates) assert result == {"key1": False, "key2": True} mock_setter1.assert_called_once_with(False) mock_setter2.assert_called_once_with(True) def test_toggle_indentation_and_justification(self, test_context: CthulhuTestContext) -> None: """Test toggle_indentation_and_justification method.""" self._setup_dependencies(test_context) from cthulhu.speech_presenter import SpeechPresenter presenter = SpeechPresenter() result = presenter.toggle_indentation_and_justification() assert result is True def test_change_number_style(self, test_context: CthulhuTestContext) -> None: """Test change_number_style method.""" self._setup_dependencies(test_context) from cthulhu.speech_presenter import SpeechPresenter presenter = SpeechPresenter() result = presenter.change_number_style() assert result is True def test_should_verbalize_punctuation_false(self, test_context: CthulhuTestContext) -> None: """Test _should_verbalize_punctuation static method returns False.""" essential_modules: dict[str, MagicMock] = self._setup_dependencies(test_context) from cthulhu.speech_presenter import SpeechPresenter essential_modules[ "cthulhu.ax_utilities" ].AXUtilities.find_ancestor_inclusive.return_value = None mock_obj = test_context.Mock() result = SpeechPresenter._should_verbalize_punctuation(mock_obj) assert result is False def test_should_verbalize_punctuation_true(self, test_context: CthulhuTestContext) -> None: """Test _should_verbalize_punctuation static method returns True.""" essential_modules: dict[str, MagicMock] = self._setup_dependencies(test_context) from cthulhu.speech_presenter import SpeechPresenter mock_code_obj = test_context.Mock() ( essential_modules["cthulhu.ax_utilities"].AXUtilities.find_ancestor_inclusive.return_value ) = mock_code_obj essential_modules["cthulhu.ax_document"].AXDocument.is_plain_text.return_value = False mock_obj = test_context.Mock() result = SpeechPresenter._should_verbalize_punctuation(mock_obj) assert result is True def test_adjust_for_verbalized_punctuation(self, test_context: CthulhuTestContext) -> None: """Test _adjust_for_verbalized_punctuation static method.""" essential_modules: dict[str, MagicMock] = self._setup_dependencies(test_context) from cthulhu.speech_presenter import SpeechPresenter mock_code_obj = test_context.Mock() ( essential_modules["cthulhu.ax_utilities"].AXUtilities.find_ancestor_inclusive.return_value ) = mock_code_obj essential_modules["cthulhu.ax_document"].AXDocument.is_plain_text.return_value = False mock_obj = test_context.Mock() text = "Hello, world! How are you?" result = SpeechPresenter._adjust_for_verbalized_punctuation(mock_obj, text) expected = "Hello , world ! How are you ? " assert result == expected @pytest.mark.parametrize( "case", [ {"id": "set_speak_blank_lines_true", "input_value": True, "expected": True}, {"id": "set_speak_blank_lines_false", "input_value": False, "expected": True}, ], ids=lambda case: case["id"], ) def test_set_speak_blank_lines(self, test_context: CthulhuTestContext, case: dict) -> None: """Test set_speak_blank_lines method.""" self._setup_dependencies(test_context) from cthulhu.speech_presenter import SpeechPresenter presenter = SpeechPresenter() result = presenter.set_speak_blank_lines(case["input_value"]) assert result == case["expected"] assert presenter.get_speak_blank_lines() == case["input_value"] def test_get_presenter_singleton(self, test_context: CthulhuTestContext) -> None: """Test get_presenter function returns the same instance.""" self._setup_dependencies(test_context) from cthulhu import speech_presenter presenter1 = speech_presenter.get_presenter() presenter2 = speech_presenter.get_presenter() assert presenter1 is presenter2 assert isinstance(presenter1, speech_presenter.SpeechPresenter) def _setup_speech_output_dependencies( self, test_context: CthulhuTestContext, ) -> dict[str, MagicMock]: """Set up additional mocks needed for speech output method testing.""" essential_modules = self._setup_dependencies(test_context) # Add speech module mock speech_mock = essential_modules["cthulhu.speech"] speech_mock.speak = test_context.Mock() speech_mock.speak_character = test_context.Mock() speech_mock.speak_key_event = test_context.Mock() # Add phonnames module mock phonnames_mock = essential_modules["cthulhu.phonnames"] phonnames_mock.get_phonetic_name = test_context.Mock(side_effect=lambda c: f"phonetic_{c}") # Add speech_manager mock speech_manager_mock = essential_modules["cthulhu.speech_manager"] speech_manager_instance = test_context.Mock() speech_manager_instance.get_speech_is_muted = test_context.Mock(return_value=False) speech_manager_instance.get_speech_is_enabled_and_not_muted = test_context.Mock( return_value=True, ) speech_manager_instance.get_capitalization_style = test_context.Mock(return_value="icon") speech_manager_instance.set_capitalization_style = test_context.Mock() speech_manager_instance.get_punctuation_level = test_context.Mock(return_value="all") speech_manager_instance.set_punctuation_level = test_context.Mock() speech_manager_mock.get_manager = test_context.Mock(return_value=speech_manager_instance) # Add script_manager mock for _get_active_script / _get_voice script_manager_mock = essential_modules["cthulhu.script_manager"] mock_script = test_context.Mock() speech_gen = test_context.Mock() speech_gen.voice = test_context.Mock(return_value=[{"family": "default"}]) speech_gen.generate_contents = test_context.Mock(return_value=["generated speech"]) mock_script.get_speech_generator = test_context.Mock(return_value=speech_gen) script_manager_instance = test_context.Mock() script_manager_instance.get_active_script = test_context.Mock(return_value=mock_script) script_manager_mock.get_manager = test_context.Mock(return_value=script_manager_instance) return essential_modules def test_get_voice_with_active_script(self, test_context: CthulhuTestContext) -> None: """Test _get_voice returns voice from active script.""" essential_modules = self._setup_speech_output_dependencies(test_context) from cthulhu.speech_presenter import SpeechPresenter presenter = SpeechPresenter() voice = presenter._get_voice(text="test") script_manager = essential_modules["cthulhu.script_manager"].get_manager() script = script_manager.get_active_script() script.get_speech_generator().voice.assert_called_once() kwargs = script.get_speech_generator().voice.call_args.kwargs assert kwargs["obj"] is None assert kwargs["string"] == "test" assert kwargs["context"].in_preferences_window is False assert voice == [{"family": "default"}] def test_get_voice_no_active_script(self, test_context: CthulhuTestContext) -> None: """Test _get_voice returns empty list when no active script.""" essential_modules = self._setup_speech_output_dependencies(test_context) script_manager = essential_modules["cthulhu.script_manager"].get_manager() script_manager.get_active_script.return_value = None from cthulhu.speech_presenter import SpeechPresenter presenter = SpeechPresenter() voice = presenter._get_voice(text="test") assert voice == [] def test_speak_message(self, test_context: CthulhuTestContext) -> None: """Test speak_message speaks text via speech module.""" essential_modules = self._setup_speech_output_dependencies(test_context) from cthulhu.speech_presenter import SpeechPresenter presenter = SpeechPresenter() presenter.speak_message("Hello world") essential_modules["cthulhu.speech"].speak.assert_called() def test_speak_message_non_string(self, test_context: CthulhuTestContext) -> None: """Test speak_message with non-string returns early.""" essential_modules = self._setup_speech_output_dependencies(test_context) from cthulhu.speech_presenter import SpeechPresenter presenter = SpeechPresenter() presenter.speak_message(123) # type: ignore essential_modules["cthulhu.debug"].print_exception.assert_called() essential_modules["cthulhu.speech"].speak.assert_not_called() def test_speak_message_only_displayed_text(self, test_context: CthulhuTestContext) -> None: """Test speak_message when only_speak_displayed_text is true.""" essential_modules = self._setup_speech_output_dependencies(test_context) from cthulhu import gsettings_registry from cthulhu.speech_presenter import SpeechPresenter gsettings_registry.get_registry().set_runtime_value( "speech", "only-speak-displayed-text", True, ) presenter = SpeechPresenter() presenter.speak_message("Hello world") essential_modules["cthulhu.speech"].speak.assert_not_called() def test_speak_character(self, test_context: CthulhuTestContext) -> None: """Test speak_character speaks a single character.""" essential_modules = self._setup_speech_output_dependencies(test_context) from cthulhu.speech_presenter import SpeechPresenter presenter = SpeechPresenter() presenter.speak_character("a") essential_modules["cthulhu.speech"].speak_character.assert_called_once() def test_spell_item(self, test_context: CthulhuTestContext) -> None: """Test spell_item speaks each character.""" essential_modules = self._setup_speech_output_dependencies(test_context) from cthulhu.speech_presenter import SpeechPresenter presenter = SpeechPresenter() presenter.spell_item("abc") assert essential_modules["cthulhu.speech"].speak_character.call_count == 3 def test_spell_phonetically(self, test_context: CthulhuTestContext) -> None: """Test spell_phonetically speaks phonetic names.""" essential_modules = self._setup_speech_output_dependencies(test_context) from cthulhu.speech_presenter import SpeechPresenter presenter = SpeechPresenter() presenter.spell_phonetically("ab") essential_modules["cthulhu.phonnames"].get_phonetic_name.assert_called() assert essential_modules["cthulhu.speech"].speak.call_count >= 2 def test_speak_contents(self, test_context: CthulhuTestContext) -> None: """Test speak_contents generates and speaks contents.""" essential_modules = self._setup_speech_output_dependencies(test_context) from cthulhu.speech_presenter import SpeechPresenter presenter = SpeechPresenter() mock_contents = [(test_context.Mock(), 0, 10, "test text")] presenter.speak_contents(mock_contents) script_manager = essential_modules["cthulhu.script_manager"].get_manager() script = script_manager.get_active_script() script.get_speech_generator().generate_contents.assert_called_once() essential_modules["cthulhu.speech"].speak.assert_called() def test_speak_contents_no_active_script(self, test_context: CthulhuTestContext) -> None: """Test speak_contents returns early when no active script.""" essential_modules = self._setup_speech_output_dependencies(test_context) script_manager = essential_modules["cthulhu.script_manager"].get_manager() script_manager.get_active_script.return_value = None from cthulhu.speech_presenter import SpeechPresenter presenter = SpeechPresenter() mock_contents = [(test_context.Mock(), 0, 10, "test text")] presenter.speak_contents(mock_contents) essential_modules["cthulhu.speech"].speak.assert_not_called() def test_present_key_event(self, test_context: CthulhuTestContext) -> None: """Test present_key_event speaks key via speech module.""" essential_modules = self._setup_speech_output_dependencies(test_context) from cthulhu.speech_presenter import SpeechPresenter presenter = SpeechPresenter() mock_event = test_context.Mock() mock_event.is_printable_key.return_value = True mock_event.get_key_name.return_value = "a" presenter.present_key_event(mock_event) essential_modules["cthulhu.speech"].speak_key_event.assert_called_once() def test_get_set_monitor_is_enabled(self, test_context: CthulhuTestContext) -> None: """Test getting and setting speech monitor enabled status.""" self._setup_dependencies(test_context) from cthulhu.speech_presenter import get_presenter presenter = get_presenter() result = presenter.set_monitor_is_enabled(True) assert result is True assert presenter.get_monitor_is_enabled() is True result = presenter.set_monitor_is_enabled(False) assert result is True assert presenter.get_monitor_is_enabled() is False def test_ensure_monitor_creates_when_enabled(self, test_context: CthulhuTestContext) -> None: """Test _ensure_monitor creates monitor on demand when enabled.""" essential_modules = self._setup_dependencies(test_context) speech_monitor_mock = essential_modules["cthulhu.speech_monitor"] from cthulhu.speech_presenter import get_presenter presenter = get_presenter() presenter.set_monitor_is_enabled(True) mock_monitor = test_context.Mock() speech_monitor_mock.SpeechMonitor.return_value = mock_monitor result = presenter._ensure_monitor() speech_monitor_mock.SpeechMonitor.assert_called_once() mock_monitor.show_all.assert_called_once() assert result is mock_monitor def test_ensure_monitor_returns_none_when_disabled(self, test_context: CthulhuTestContext) -> None: """Test _ensure_monitor returns None when disabled.""" self._setup_dependencies(test_context) from cthulhu.speech_presenter import get_presenter presenter = get_presenter() presenter.set_monitor_is_enabled(False) mock_monitor = test_context.Mock() presenter._monitor = mock_monitor result = presenter._ensure_monitor() assert result is None def test_write_to_monitor(self, test_context: CthulhuTestContext) -> None: """Test write_to_monitor writes text when monitor active and not focused.""" essential_modules = self._setup_dependencies(test_context) speech_monitor_mock = essential_modules["cthulhu.speech_monitor"] from cthulhu.speech_presenter import get_presenter presenter = get_presenter() presenter.set_monitor_is_enabled(True) mock_monitor = test_context.Mock() mock_monitor.has_toplevel_focus.return_value = False speech_monitor_mock.SpeechMonitor.return_value = mock_monitor presenter.write_to_monitor("hello world") mock_monitor.write_text.assert_called_once_with("hello world") def test_write_to_monitor_skips_when_focused(self, test_context: CthulhuTestContext) -> None: """Test write_to_monitor skips when monitor has focus.""" essential_modules = self._setup_dependencies(test_context) speech_monitor_mock = essential_modules["cthulhu.speech_monitor"] from cthulhu.speech_presenter import get_presenter presenter = get_presenter() presenter.set_monitor_is_enabled(True) mock_monitor = test_context.Mock() mock_monitor.has_toplevel_focus.return_value = True speech_monitor_mock.SpeechMonitor.return_value = mock_monitor presenter.write_to_monitor("hello world") mock_monitor.write_text.assert_not_called() def test_write_key_to_monitor(self, test_context: CthulhuTestContext) -> None: """Test write_key_to_monitor writes key event.""" essential_modules = self._setup_dependencies(test_context) speech_monitor_mock = essential_modules["cthulhu.speech_monitor"] from cthulhu.speech_presenter import get_presenter presenter = get_presenter() presenter.set_monitor_is_enabled(True) mock_monitor = test_context.Mock() mock_monitor.has_toplevel_focus.return_value = False speech_monitor_mock.SpeechMonitor.return_value = mock_monitor presenter.write_key_to_monitor("Return") mock_monitor.write_key_event.assert_called_once_with("Return") def test_destroy_monitor(self, test_context: CthulhuTestContext) -> None: """Test destroy_monitor destroys existing speech monitor.""" self._setup_dependencies(test_context) from cthulhu.speech_presenter import get_presenter presenter = get_presenter() mock_monitor = test_context.Mock() presenter._monitor = mock_monitor presenter.destroy_monitor() mock_monitor.destroy.assert_called_once() assert presenter._monitor is None def test_destroy_monitor_no_op_when_none(self, test_context: CthulhuTestContext) -> None: """Test destroy_monitor does nothing when no monitor exists.""" self._setup_dependencies(test_context) from cthulhu.speech_presenter import get_presenter presenter = get_presenter() assert presenter._monitor is None presenter.destroy_monitor() assert presenter._monitor is None