Files
cthulhu/tests/test_speech_presenter.py

1096 lines
43 KiB
Python

# Unit tests for speech_presenter.py methods.
#
# Copyright 2025 Igalia, S.L.
# Author: Joanmarie Diggs <jdiggs@igalia.com>
#
# 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