937 lines
40 KiB
Python
937 lines
40 KiB
Python
# Unit tests for typing_echo_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=protected-access
|
|
# pylint: disable=too-many-public-methods
|
|
# pylint: disable=too-many-arguments
|
|
# pylint: disable=too-many-positional-arguments
|
|
# pylint: disable=too-many-lines
|
|
|
|
"""Unit tests for typing_echo_presenter.py methods."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING
|
|
from unittest.mock import Mock
|
|
|
|
import pytest
|
|
|
|
if TYPE_CHECKING:
|
|
from typing import ClassVar
|
|
|
|
from cthulhu_test_context import CthulhuTestContext
|
|
|
|
|
|
class _FakeGtkGrid:
|
|
"""Minimal stub used in unit tests."""
|
|
|
|
def __init__(self, *_args, **_kwargs):
|
|
"""Initialize fake GTK grid."""
|
|
self._children = []
|
|
|
|
def attach(self, *args, **kwargs):
|
|
"""Attach widget to grid."""
|
|
self._children.append((args, kwargs))
|
|
|
|
def set_border_width(self, *_args, **_kwargs):
|
|
"""Set border width."""
|
|
return None
|
|
|
|
def set_margin_start(self, *_args, **_kwargs):
|
|
"""Set margin start."""
|
|
return None
|
|
|
|
def show_all(self):
|
|
"""Show all widgets."""
|
|
return None
|
|
|
|
|
|
class _FakeCheckButton:
|
|
"""Minimal stub emulating Gtk.CheckButton for unit tests."""
|
|
|
|
def __init__(self, label: str):
|
|
self.label = label
|
|
self.name = label
|
|
self._active = False
|
|
self._signal_handlers: dict[str, tuple] = {}
|
|
|
|
@classmethod
|
|
def new_with_mnemonic(cls, label: str) -> _FakeCheckButton:
|
|
"""Create new check button with mnemonic."""
|
|
return cls(label)
|
|
|
|
def set_name(self, name: str) -> None:
|
|
"""Set widget name."""
|
|
self.name = name
|
|
|
|
def set_use_underline(self, *_args, **_kwargs) -> None: # pragma: no cover - unused
|
|
"""Set use underline."""
|
|
return None
|
|
|
|
def set_receives_default(self, *_args, **_kwargs) -> None: # pragma: no cover
|
|
"""Set receives default."""
|
|
return None
|
|
|
|
def connect(self, signal: str, handler, data) -> None:
|
|
"""Connect signal handler."""
|
|
self._signal_handlers[signal] = (handler, data)
|
|
|
|
def set_active(self, active: bool) -> None:
|
|
"""Set active state."""
|
|
self._active = active
|
|
|
|
def get_active(self) -> bool: # pragma: no cover - helper when needed
|
|
"""Get active state."""
|
|
return self._active
|
|
|
|
def set_sensitive(self, _sensitive: bool) -> None:
|
|
"""Set sensitive state."""
|
|
return None
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestTypingEchoPresenter:
|
|
"""Test TypingEchoPresenter and TypingEchoPreferencesGrid."""
|
|
|
|
_CMDNAME_VALUES: ClassVar[dict[str, str]] = {
|
|
"STRUCTURAL_NAVIGATION_MODE_CYCLE": "cycle_mode",
|
|
"BLOCKQUOTE_PREV": "previous_blockquote",
|
|
"BLOCKQUOTE_NEXT": "next_blockquote",
|
|
"BLOCKQUOTE_LIST": "list_blockquotes",
|
|
"BUTTON_PREV": "previous_button",
|
|
"BUTTON_NEXT": "next_button",
|
|
"BUTTON_LIST": "list_buttons",
|
|
"CHECK_BOX_PREV": "previous_checkbox",
|
|
"CHECK_BOX_NEXT": "next_checkbox",
|
|
"CHECK_BOX_LIST": "list_checkboxes",
|
|
"COMBO_BOX_PREV": "previous_combobox",
|
|
"COMBO_BOX_NEXT": "next_combobox",
|
|
"COMBO_BOX_LIST": "list_comboboxes",
|
|
"ENTRY_PREV": "previous_entry",
|
|
"ENTRY_NEXT": "next_entry",
|
|
"ENTRY_LIST": "list_entries",
|
|
"FORM_FIELD_PREV": "previous_form_field",
|
|
"FORM_FIELD_NEXT": "next_form_field",
|
|
"FORM_FIELD_LIST": "list_form_fields",
|
|
"HEADING_PREV": "previous_heading",
|
|
"HEADING_NEXT": "next_heading",
|
|
"HEADING_LIST": "list_headings",
|
|
"HEADING_AT_LEVEL_PREV": "previous_heading_level_%d",
|
|
"HEADING_AT_LEVEL_NEXT": "next_heading_level_%d",
|
|
"HEADING_AT_LEVEL_LIST": "list_headings_level_%d",
|
|
"IFRAME_PREV": "previous_iframe",
|
|
"IFRAME_NEXT": "next_iframe",
|
|
"IFRAME_LIST": "list_iframes",
|
|
"IMAGE_PREV": "previous_image",
|
|
"IMAGE_NEXT": "next_image",
|
|
"IMAGE_LIST": "list_images",
|
|
"LANDMARK_PREV": "previous_landmark",
|
|
"LANDMARK_NEXT": "next_landmark",
|
|
"LANDMARK_LIST": "list_landmarks",
|
|
"LIST_PREV": "previous_list",
|
|
"LIST_NEXT": "next_list",
|
|
"LIST_LIST": "list_lists",
|
|
"LIST_ITEM_PREV": "previous_list_item",
|
|
"LIST_ITEM_NEXT": "next_list_item",
|
|
"LIST_ITEM_LIST": "list_list_items",
|
|
"LIVE_REGION_PREV": "previous_live_region",
|
|
"LIVE_REGION_NEXT": "next_live_region",
|
|
"LIVE_REGION_LAST": "last_live_region",
|
|
"PARAGRAPH_PREV": "previous_paragraph",
|
|
"PARAGRAPH_NEXT": "next_paragraph",
|
|
"PARAGRAPH_LIST": "list_paragraphs",
|
|
"RADIO_BUTTON_PREV": "previous_radio_button",
|
|
"RADIO_BUTTON_NEXT": "next_radio_button",
|
|
"RADIO_BUTTON_LIST": "list_radio_buttons",
|
|
"SEPARATOR_PREV": "previous_separator",
|
|
"SEPARATOR_NEXT": "next_separator",
|
|
"TABLE_PREV": "previous_table",
|
|
"TABLE_NEXT": "next_table",
|
|
"TABLE_LIST": "list_tables",
|
|
"UNVISITED_LINK_PREV": "previous_unvisited_link",
|
|
"UNVISITED_LINK_NEXT": "next_unvisited_link",
|
|
"UNVISITED_LINK_LIST": "list_unvisited_links",
|
|
"VISITED_LINK_PREV": "previous_visited_link",
|
|
"VISITED_LINK_NEXT": "next_visited_link",
|
|
"VISITED_LINK_LIST": "list_visited_links",
|
|
"LINK_PREV": "previous_link",
|
|
"LINK_NEXT": "next_link",
|
|
"LINK_LIST": "list_links",
|
|
"CLICKABLE_PREV": "previous_clickable",
|
|
"CLICKABLE_NEXT": "next_clickable",
|
|
"CLICKABLE_LIST": "list_clickables",
|
|
"LARGE_OBJECT_PREV": "previous_large_object",
|
|
"LARGE_OBJECT_NEXT": "next_large_object",
|
|
"LARGE_OBJECT_LIST": "list_large_objects",
|
|
"CONTAINER_START": "container_start",
|
|
"CONTAINER_END": "container_end",
|
|
}
|
|
|
|
@staticmethod
|
|
def _setup_cmdnames(cmdnames) -> None:
|
|
"""Set up cmdnames with all required values for structural_navigator."""
|
|
|
|
for attr, value in TestTypingEchoPresenter._CMDNAME_VALUES.items():
|
|
setattr(cmdnames, attr, value)
|
|
|
|
@staticmethod
|
|
def _setup_guilabels(guilabels_mock) -> None:
|
|
"""Set up guilabels mock with typing echo label values."""
|
|
|
|
guilabels_mock.ECHO_ENABLE_KEY_ECHO = "Enable _key echo"
|
|
guilabels_mock.ECHO_ALPHABETIC_KEYS = "Enable _alphabetic keys"
|
|
guilabels_mock.ECHO_NUMERIC_KEYS = "Enable n_umeric keys"
|
|
guilabels_mock.ECHO_PUNCTUATION_KEYS = "Enable _punctuation keys"
|
|
guilabels_mock.ECHO_SPACE = "Enable _space"
|
|
guilabels_mock.ECHO_MODIFIER_KEYS = "Enable _modifier keys"
|
|
guilabels_mock.ECHO_FUNCTION_KEYS = "Enable _function keys"
|
|
guilabels_mock.ECHO_ACTION_KEYS = "Enable ac_tion keys"
|
|
guilabels_mock.ECHO_NAVIGATION_KEYS = "Enable _navigation keys"
|
|
guilabels_mock.ECHO_DIACRITICAL_KEYS = "Enable non-spacing _diacritical keys"
|
|
guilabels_mock.ECHO_CHARACTER = "Enable echo by cha_racter"
|
|
guilabels_mock.ECHO_WORD = "Enable echo by _word"
|
|
guilabels_mock.ECHO_SENTENCE = "Enable echo by _sentence"
|
|
|
|
@staticmethod
|
|
def _setup_atspi_patches(test_context: CthulhuTestContext) -> None:
|
|
"""Set up Atspi type patches for testing."""
|
|
|
|
from gi.repository import Atspi
|
|
|
|
test_context.patch_object(Atspi, "Accessible", new=type("Accessible", (), {}))
|
|
test_context.patch_object(Atspi, "Hyperlink", new=type("Hyperlink", (), {}))
|
|
test_context.patch_object(
|
|
Atspi,
|
|
"Role",
|
|
new=type("Role", (), {"PASSWORD_TEXT": 42, "PANEL": 36}),
|
|
)
|
|
test_context.patch_object(
|
|
Atspi,
|
|
"CollectionMatchType",
|
|
new=type("CollectionMatchType", (), {"ALL": 0, "ANY": 1}),
|
|
)
|
|
test_context.patch_object(Atspi, "MatchRule", new=type("MatchRule", (), {}))
|
|
test_context.patch_object(
|
|
Atspi,
|
|
"RelationType",
|
|
new=type("RelationType", (), {"LABELLED_BY": 0}),
|
|
)
|
|
test_context.patch_object(Atspi, "Relation", new=type("Relation", (), {}))
|
|
|
|
_ADDITIONAL_MODULES: ClassVar[list[str]] = [
|
|
"gi",
|
|
"gi.repository",
|
|
"cthulhu.cmdnames",
|
|
"cthulhu.messages",
|
|
"cthulhu.object_properties",
|
|
"cthulhu.cthulhu_gui_navlist",
|
|
"cthulhu.cthulhu_i18n",
|
|
"cthulhu.AXHypertext",
|
|
"cthulhu.AXObject",
|
|
"cthulhu.AXTable",
|
|
"cthulhu.AXText",
|
|
"cthulhu.AXUtilities",
|
|
"cthulhu.input_event",
|
|
"cthulhu.braille_presenter",
|
|
"cthulhu.presentation_manager",
|
|
"cthulhu.speech_presenter",
|
|
]
|
|
|
|
_DEFAULT_VALUES: ClassVar[dict[str, bool]] = {
|
|
"enableKeyEcho": True,
|
|
"enableAlphabeticKeys": True,
|
|
"enableNumericKeys": True,
|
|
"enablePunctuationKeys": True,
|
|
"enableSpace": True,
|
|
"enableModifierKeys": True,
|
|
"enableFunctionKeys": True,
|
|
"enableActionKeys": True,
|
|
"enableNavigationKeys": False,
|
|
"enableDiacriticalKeys": False,
|
|
"enableEchoByCharacter": False,
|
|
"enableEchoByWord": False,
|
|
"enableEchoBySentence": False,
|
|
}
|
|
|
|
@staticmethod
|
|
def _setup_essential_mocks(test_context: CthulhuTestContext, essential_modules: dict) -> None:
|
|
"""Set up essential mock objects for testing."""
|
|
|
|
essential_modules["cthulhu.cthulhu_i18n"]._ = lambda x: x
|
|
essential_modules["cthulhu.debug"].print_message = test_context.Mock()
|
|
essential_modules["cthulhu.debug"].LEVEL_INFO = 800
|
|
essential_modules["cthulhu.debug"].LEVEL_SEVERE = 1000
|
|
essential_modules["cthulhu.debug"].debugLevel = 1000
|
|
|
|
controller_mock = test_context.Mock()
|
|
controller_mock.register_decorated_module.return_value = None
|
|
essential_modules["cthulhu.dbus_service"].get_remote_controller.return_value = controller_mock
|
|
|
|
focus_manager_instance = test_context.Mock()
|
|
focus_manager_instance.get_locus_of_focus.return_value = None
|
|
essential_modules["cthulhu.focus_manager"].get_manager.return_value = focus_manager_instance
|
|
|
|
essential_modules["cthulhu.AXObject"].supports_collection.return_value = True
|
|
essential_modules["cthulhu.AXUtilities"].is_heading.return_value = False
|
|
|
|
test_context.patch("gi.repository.Gtk.Grid", new=_FakeGtkGrid)
|
|
test_context.patch("gi.repository.Gtk.CheckButton", new=_FakeCheckButton)
|
|
|
|
def _setup_presenter(self, test_context: CthulhuTestContext):
|
|
"""Set up presenter and dependencies for testing."""
|
|
|
|
essential_modules = test_context.setup_shared_dependencies(self._ADDITIONAL_MODULES)
|
|
|
|
self._setup_cmdnames(essential_modules["cthulhu.cmdnames"])
|
|
self._setup_essential_mocks(test_context, essential_modules)
|
|
self._setup_guilabels(essential_modules["cthulhu.guilabels"])
|
|
|
|
self._setup_atspi_patches(test_context)
|
|
|
|
from cthulhu import gsettings_registry
|
|
from cthulhu.typing_echo_presenter import TypingEchoPresenter
|
|
|
|
registry = gsettings_registry.get_registry()
|
|
registry.clear_runtime_values()
|
|
|
|
presenter = TypingEchoPresenter()
|
|
return presenter
|
|
|
|
@pytest.mark.parametrize(
|
|
"getter_name,setter_name,setting_key,test_value",
|
|
[
|
|
("get_key_echo_enabled", "set_key_echo_enabled", "enableKeyEcho", False),
|
|
(
|
|
"get_character_echo_enabled",
|
|
"set_character_echo_enabled",
|
|
"enableEchoByCharacter",
|
|
False,
|
|
),
|
|
("get_word_echo_enabled", "set_word_echo_enabled", "enableEchoByWord", True),
|
|
(
|
|
"get_sentence_echo_enabled",
|
|
"set_sentence_echo_enabled",
|
|
"enableEchoBySentence",
|
|
False,
|
|
),
|
|
(
|
|
"get_alphabetic_keys_enabled",
|
|
"set_alphabetic_keys_enabled",
|
|
"enableAlphabeticKeys",
|
|
False,
|
|
),
|
|
("get_numeric_keys_enabled", "set_numeric_keys_enabled", "enableNumericKeys", True),
|
|
(
|
|
"get_punctuation_keys_enabled",
|
|
"set_punctuation_keys_enabled",
|
|
"enablePunctuationKeys",
|
|
True,
|
|
),
|
|
("get_space_enabled", "set_space_enabled", "enableSpace", False),
|
|
("get_modifier_keys_enabled", "set_modifier_keys_enabled", "enableModifierKeys", True),
|
|
("get_function_keys_enabled", "set_function_keys_enabled", "enableFunctionKeys", False),
|
|
("get_action_keys_enabled", "set_action_keys_enabled", "enableActionKeys", True),
|
|
(
|
|
"get_navigation_keys_enabled",
|
|
"set_navigation_keys_enabled",
|
|
"enableNavigationKeys",
|
|
False,
|
|
),
|
|
(
|
|
"get_diacritical_keys_enabled",
|
|
"set_diacritical_keys_enabled",
|
|
"enableDiacriticalKeys",
|
|
True,
|
|
),
|
|
],
|
|
)
|
|
def test_presenter_getters_and_setters(
|
|
self,
|
|
test_context: CthulhuTestContext,
|
|
getter_name: str,
|
|
setter_name: str,
|
|
setting_key: str,
|
|
test_value: bool,
|
|
) -> None:
|
|
"""Test presenter getter and setter methods."""
|
|
presenter = self._setup_presenter(test_context)
|
|
|
|
getter = getattr(presenter, getter_name)
|
|
setter = getattr(presenter, setter_name)
|
|
|
|
assert getter() is self._DEFAULT_VALUES[setting_key]
|
|
setter(test_value)
|
|
assert getter() == test_value
|
|
|
|
def test_locking_keys_presented_getter_and_setter(self, test_context: CthulhuTestContext) -> None:
|
|
"""Test locking keys presented getter and setter with special logic."""
|
|
presenter = self._setup_presenter(test_context)
|
|
|
|
presenter._present_locking_keys = True
|
|
assert presenter.get_locking_keys_presented() is True
|
|
|
|
presenter._present_locking_keys = False
|
|
assert presenter.get_locking_keys_presented() is False
|
|
|
|
presenter._present_locking_keys = None
|
|
|
|
speech_presenter_patch = test_context.patch("cthulhu.speech_presenter.get_presenter")
|
|
speech_presenter_instance = speech_presenter_patch.return_value
|
|
|
|
speech_presenter_instance.get_only_speak_displayed_text.return_value = False
|
|
assert presenter.get_locking_keys_presented() is True
|
|
|
|
speech_presenter_instance.get_only_speak_displayed_text.return_value = True
|
|
assert presenter.get_locking_keys_presented() is False
|
|
|
|
presenter.set_locking_keys_presented(True)
|
|
assert presenter._present_locking_keys is True
|
|
|
|
presenter.set_locking_keys_presented(None)
|
|
assert presenter._present_locking_keys is None
|
|
|
|
def test_cycle_key_echo_basic_transitions(self, test_context: CthulhuTestContext) -> None:
|
|
"""Test cycle_key_echo method basic state transitions."""
|
|
presenter = self._setup_presenter(test_context)
|
|
|
|
script_mock = test_context.mocker.MagicMock()
|
|
|
|
presenter.set_key_echo_enabled(False)
|
|
|
|
result = presenter.cycle_key_echo(script_mock, None, True)
|
|
assert result is True
|
|
assert presenter.get_key_echo_enabled() is True
|
|
assert presenter.get_word_echo_enabled() is False
|
|
assert presenter.get_sentence_echo_enabled() is False
|
|
|
|
result = presenter.cycle_key_echo(script_mock, None, True)
|
|
assert result is True
|
|
assert presenter.get_key_echo_enabled() is False
|
|
assert presenter.get_word_echo_enabled() is True
|
|
assert presenter.get_sentence_echo_enabled() is False
|
|
|
|
def test_cycle_key_echo_advanced_transitions(self, test_context: CthulhuTestContext) -> None:
|
|
"""Test cycle_key_echo method advanced state transitions."""
|
|
presenter = self._setup_presenter(test_context)
|
|
|
|
script_mock = test_context.mocker.MagicMock()
|
|
|
|
presenter.set_key_echo_enabled(False)
|
|
presenter.set_word_echo_enabled(True)
|
|
|
|
result = presenter.cycle_key_echo(script_mock, None, True)
|
|
assert result is True
|
|
assert presenter.get_key_echo_enabled() is False
|
|
assert presenter.get_word_echo_enabled() is False
|
|
assert presenter.get_sentence_echo_enabled() is True
|
|
|
|
result = presenter.cycle_key_echo(script_mock, None, True)
|
|
assert result is True
|
|
assert presenter.get_key_echo_enabled() is True
|
|
assert presenter.get_word_echo_enabled() is True
|
|
assert presenter.get_sentence_echo_enabled() is False
|
|
|
|
result = presenter.cycle_key_echo(script_mock, None, True)
|
|
assert result is True
|
|
assert presenter.get_key_echo_enabled() is False
|
|
assert presenter.get_word_echo_enabled() is True
|
|
assert presenter.get_sentence_echo_enabled() is True
|
|
|
|
result = presenter.cycle_key_echo(script_mock, None, True)
|
|
assert result is True
|
|
assert presenter.get_key_echo_enabled() is False
|
|
assert presenter.get_word_echo_enabled() is False
|
|
assert presenter.get_sentence_echo_enabled() is False
|
|
|
|
def test_cycle_key_echo_with_script_presentation(self, test_context: CthulhuTestContext) -> None:
|
|
"""Test cycle_key_echo calls present_message when script is provided."""
|
|
presenter = self._setup_presenter(test_context)
|
|
|
|
script_mock = test_context.mocker.MagicMock()
|
|
|
|
presenter.set_key_echo_enabled(False)
|
|
|
|
from cthulhu import presentation_manager
|
|
|
|
present_msg = presentation_manager.get_manager().present_message
|
|
assert isinstance(present_msg, Mock)
|
|
present_msg.reset_mock() # pylint: disable=no-member
|
|
|
|
presenter.cycle_key_echo(script_mock, None, True)
|
|
assert present_msg.call_count == 1 # pylint: disable=no-member
|
|
|
|
present_msg.reset_mock() # pylint: disable=no-member
|
|
presenter.cycle_key_echo(script_mock, None, False)
|
|
assert present_msg.call_count == 0 # pylint: disable=no-member
|
|
|
|
presenter.cycle_key_echo(None, None, True)
|
|
|
|
def test_should_echo_keyboard_event_basic_cases(self, test_context: CthulhuTestContext) -> None:
|
|
"""Test should_echo_keyboard_event for basic cases."""
|
|
presenter = self._setup_presenter(test_context)
|
|
|
|
event_mock = test_context.mocker.MagicMock()
|
|
event_mock.should_obscure.return_value = False
|
|
event_mock.get_key_name.return_value = "a"
|
|
|
|
event_mock.is_pressed_key.return_value = False
|
|
assert presenter.should_echo_keyboard_event(event_mock) is False
|
|
|
|
event_mock.is_pressed_key.return_value = True
|
|
presenter.set_key_echo_enabled(False)
|
|
event_mock.is_cthulhu_modifier.return_value = False
|
|
event_mock.is_alt_control_or_cthulhu_modified.return_value = False
|
|
event_mock.is_locking_key.return_value = False
|
|
presenter.is_character_echoable = test_context.mocker.MagicMock(return_value=False)
|
|
assert presenter.should_echo_keyboard_event(event_mock) is False
|
|
|
|
def test_should_echo_keyboard_event_cthulhu_modifier(self, test_context: CthulhuTestContext) -> None:
|
|
"""Test should_echo_keyboard_event for Cthulhu modifier keys."""
|
|
presenter = self._setup_presenter(test_context)
|
|
|
|
event_mock = test_context.mocker.MagicMock()
|
|
event_mock.should_obscure.return_value = False
|
|
event_mock.get_key_name.return_value = "Insert_L"
|
|
event_mock.is_pressed_key.return_value = True
|
|
event_mock.is_cthulhu_modifier.return_value = True
|
|
|
|
event_mock.get_click_count.return_value = 2
|
|
assert presenter.should_echo_keyboard_event(event_mock) is True
|
|
|
|
event_mock.get_click_count.return_value = 1
|
|
assert presenter.should_echo_keyboard_event(event_mock) is True
|
|
|
|
presenter.set_modifier_keys_enabled(False)
|
|
assert presenter.should_echo_keyboard_event(event_mock) is False
|
|
|
|
def test_should_echo_keyboard_event_modified_keys(self, test_context: CthulhuTestContext) -> None:
|
|
"""Test should_echo_keyboard_event for modified keys."""
|
|
presenter = self._setup_presenter(test_context)
|
|
|
|
event_mock = test_context.mocker.MagicMock()
|
|
event_mock.should_obscure.return_value = False
|
|
event_mock.get_key_name.return_value = "a"
|
|
event_mock.is_pressed_key.return_value = True
|
|
event_mock.is_cthulhu_modifier.return_value = False
|
|
|
|
event_mock.is_alt_control_or_cthulhu_modified.return_value = True
|
|
assert presenter.should_echo_keyboard_event(event_mock) is False
|
|
|
|
def test_should_echo_keyboard_event_character_echoable(
|
|
self,
|
|
test_context: CthulhuTestContext,
|
|
) -> None:
|
|
"""Test should_echo_keyboard_event when character is echoable."""
|
|
presenter = self._setup_presenter(test_context)
|
|
|
|
event_mock = test_context.mocker.MagicMock()
|
|
event_mock.should_obscure.return_value = False
|
|
event_mock.get_key_name.return_value = "a"
|
|
event_mock.is_pressed_key.return_value = True
|
|
event_mock.is_cthulhu_modifier.return_value = False
|
|
event_mock.is_alt_control_or_cthulhu_modified.return_value = False
|
|
|
|
presenter.is_character_echoable = test_context.mocker.MagicMock(return_value=True)
|
|
assert presenter.should_echo_keyboard_event(event_mock) is False
|
|
|
|
@pytest.mark.parametrize(
|
|
"key_type,_setting_key,expected_result",
|
|
[
|
|
# Standard key types - enabled
|
|
("alphabetic", "enableAlphabeticKeys", True),
|
|
("numeric", "enableNumericKeys", True),
|
|
("punctuation", "enablePunctuationKeys", True),
|
|
("modifier", "enableModifierKeys", True),
|
|
("function", "enableFunctionKeys", True),
|
|
("action", "enableActionKeys", True),
|
|
("navigation", "enableNavigationKeys", True),
|
|
("diacritical", "enableDiacriticalKeys", True),
|
|
# Standard key types - disabled
|
|
("alphabetic", "enableAlphabeticKeys", False),
|
|
("numeric", "enableNumericKeys", False),
|
|
("punctuation", "enablePunctuationKeys", False),
|
|
("modifier", "enableModifierKeys", False),
|
|
("function", "enableFunctionKeys", False),
|
|
("action", "enableActionKeys", False),
|
|
("navigation", "enableNavigationKeys", False),
|
|
("diacritical", "enableDiacriticalKeys", False),
|
|
],
|
|
)
|
|
def test_should_echo_keyboard_event_key_types(
|
|
self,
|
|
test_context: CthulhuTestContext,
|
|
key_type: str,
|
|
_setting_key: str,
|
|
expected_result: bool,
|
|
) -> None:
|
|
"""Test should_echo_keyboard_event for different key types."""
|
|
presenter = self._setup_presenter(test_context)
|
|
|
|
from cthulhu import gsettings_registry
|
|
|
|
event_mock = test_context.mocker.MagicMock()
|
|
event_mock.should_obscure.return_value = False
|
|
event_mock.get_key_name.return_value = "test_key"
|
|
event_mock.is_pressed_key.return_value = True
|
|
event_mock.is_cthulhu_modifier.return_value = False
|
|
event_mock.is_alt_control_or_cthulhu_modified.return_value = False
|
|
event_mock.is_locking_key.return_value = False
|
|
event_mock.is_space.return_value = False
|
|
presenter.is_character_echoable = test_context.mocker.MagicMock(return_value=False)
|
|
|
|
# Set all key type checks to False first
|
|
event_mock.is_alphabetic_key.return_value = False
|
|
event_mock.is_numeric_key.return_value = False
|
|
event_mock.is_punctuation_key.return_value = False
|
|
event_mock.is_modifier_key.return_value = False
|
|
event_mock.is_function_key.return_value = False
|
|
event_mock.is_action_key.return_value = False
|
|
event_mock.is_navigation_key.return_value = False
|
|
event_mock.is_diacritical_key.return_value = False
|
|
|
|
# Enable the specific key type being tested
|
|
setattr(event_mock, f"is_{key_type}_key", lambda: True)
|
|
|
|
gs_key = f"{key_type}-keys"
|
|
gsettings_registry.get_registry().set_runtime_value("typing-echo", gs_key, expected_result)
|
|
|
|
result = presenter.should_echo_keyboard_event(event_mock)
|
|
assert result is expected_result
|
|
|
|
def test_should_echo_keyboard_event_space_key_scenarios(
|
|
self,
|
|
test_context: CthulhuTestContext,
|
|
) -> None:
|
|
"""Test should_echo_keyboard_event for space key with different settings."""
|
|
presenter = self._setup_presenter(test_context)
|
|
|
|
event_mock = test_context.mocker.MagicMock()
|
|
event_mock.should_obscure.return_value = False
|
|
event_mock.get_key_name.return_value = "space"
|
|
event_mock.is_pressed_key.return_value = True
|
|
event_mock.is_cthulhu_modifier.return_value = False
|
|
event_mock.is_alt_control_or_cthulhu_modified.return_value = False
|
|
event_mock.is_locking_key.return_value = False
|
|
event_mock.is_space.return_value = True
|
|
presenter.is_character_echoable = test_context.mocker.MagicMock(return_value=False)
|
|
|
|
# Set all other key types to False
|
|
event_mock.is_alphabetic_key.return_value = False
|
|
event_mock.is_numeric_key.return_value = False
|
|
event_mock.is_punctuation_key.return_value = False
|
|
event_mock.is_modifier_key.return_value = False
|
|
event_mock.is_function_key.return_value = False
|
|
event_mock.is_action_key.return_value = False
|
|
event_mock.is_navigation_key.return_value = False
|
|
event_mock.is_diacritical_key.return_value = False
|
|
|
|
# Test space key with space setting enabled (default), character echo disabled (default)
|
|
assert presenter.should_echo_keyboard_event(event_mock) is True
|
|
|
|
# Test space key with space disabled but character echo enabled (should echo)
|
|
presenter.set_space_enabled(False)
|
|
presenter.set_character_echo_enabled(True)
|
|
assert presenter.should_echo_keyboard_event(event_mock) is True
|
|
|
|
# Test space key with both space and character echo disabled
|
|
presenter.set_character_echo_enabled(False)
|
|
assert presenter.should_echo_keyboard_event(event_mock) is False
|
|
|
|
def test_should_echo_keyboard_event_locking_keys(self, test_context: CthulhuTestContext) -> None:
|
|
"""Test should_echo_keyboard_event for locking keys."""
|
|
presenter = self._setup_presenter(test_context)
|
|
|
|
event_mock = test_context.mocker.MagicMock()
|
|
event_mock.should_obscure.return_value = False
|
|
event_mock.get_key_name.return_value = "Caps_Lock"
|
|
event_mock.is_pressed_key.return_value = True
|
|
event_mock.is_cthulhu_modifier.return_value = False
|
|
event_mock.is_alt_control_or_cthulhu_modified.return_value = False
|
|
event_mock.is_locking_key.return_value = True
|
|
presenter.is_character_echoable = test_context.mocker.MagicMock(return_value=False)
|
|
|
|
# Test locking key when locking keys are presented
|
|
presenter._present_locking_keys = True
|
|
assert presenter.should_echo_keyboard_event(event_mock) is True
|
|
|
|
# Test locking key when locking keys are not presented
|
|
presenter._present_locking_keys = False
|
|
assert presenter.should_echo_keyboard_event(event_mock) is False
|
|
|
|
def test_should_echo_keyboard_event_password_text_obscuring(
|
|
self,
|
|
test_context: CthulhuTestContext,
|
|
) -> None:
|
|
"""Test should_echo_keyboard_event with password text that should be obscured."""
|
|
presenter = self._setup_presenter(test_context)
|
|
|
|
event_mock = test_context.mocker.MagicMock()
|
|
event_mock.get_key_name.return_value = "a"
|
|
event_mock.is_pressed_key.return_value = True
|
|
event_mock.is_cthulhu_modifier.return_value = False
|
|
event_mock.is_alt_control_or_cthulhu_modified.return_value = False
|
|
event_mock.is_locking_key.return_value = False
|
|
# Set all other key type checks to False so we get to password text check
|
|
event_mock.is_navigation_key.return_value = False
|
|
event_mock.is_action_key.return_value = False
|
|
event_mock.is_modifier_key.return_value = False
|
|
event_mock.is_function_key.return_value = False
|
|
event_mock.is_diacritical_key.return_value = False
|
|
event_mock.is_alphabetic_key.return_value = True
|
|
presenter.is_character_echoable = test_context.mocker.MagicMock(return_value=False)
|
|
|
|
# Test with password text that should be obscured - should not echo
|
|
event_mock.should_obscure.return_value = True
|
|
test_context.patch("cthulhu.ax_utilities.AXUtilities.is_password_text", return_value=True)
|
|
assert presenter.should_echo_keyboard_event(event_mock) is False
|
|
|
|
# Test with password text that should not be obscured - should echo alphabetic
|
|
event_mock.should_obscure.return_value = False
|
|
test_context.patch("cthulhu.ax_utilities.AXUtilities.is_password_text", return_value=False)
|
|
assert presenter.should_echo_keyboard_event(event_mock) is True
|
|
|
|
def test_is_character_echoable(self, test_context: CthulhuTestContext) -> None:
|
|
"""Test is_character_echoable method."""
|
|
presenter = self._setup_presenter(test_context)
|
|
|
|
event_mock = test_context.mocker.MagicMock()
|
|
|
|
assert presenter.is_character_echoable(event_mock) is False
|
|
|
|
presenter.set_character_echo_enabled(True)
|
|
|
|
event_mock.is_alt_control_or_cthulhu_modified.return_value = True
|
|
assert presenter.is_character_echoable(event_mock) is False
|
|
|
|
event_mock.is_alt_control_or_cthulhu_modified.return_value = False
|
|
event_mock.is_printable_key.return_value = False
|
|
assert presenter.is_character_echoable(event_mock) is False
|
|
|
|
event_mock.is_printable_key.return_value = True
|
|
obj_mock = test_context.mocker.MagicMock()
|
|
event_mock.get_object.return_value = obj_mock
|
|
|
|
test_context.patch("cthulhu.ax_utilities.AXUtilities.is_password_text", return_value=True)
|
|
assert presenter.is_character_echoable(event_mock) is False
|
|
|
|
test_context.patch("cthulhu.ax_utilities.AXUtilities.is_password_text", return_value=False)
|
|
test_context.patch("cthulhu.ax_utilities.AXUtilities.is_editable", return_value=True)
|
|
test_context.patch("cthulhu.ax_utilities.AXUtilities.is_terminal", return_value=False)
|
|
assert presenter.is_character_echoable(event_mock) is True
|
|
|
|
test_context.patch("cthulhu.ax_utilities.AXUtilities.is_editable", return_value=False)
|
|
test_context.patch("cthulhu.ax_utilities.AXUtilities.is_terminal", return_value=True)
|
|
assert presenter.is_character_echoable(event_mock) is True
|
|
|
|
test_context.patch("cthulhu.ax_utilities.AXUtilities.is_terminal", return_value=False)
|
|
assert presenter.is_character_echoable(event_mock) is False
|
|
|
|
def test_echo_previous_word(self, test_context: CthulhuTestContext) -> None:
|
|
"""Test echo_previous_word method."""
|
|
presenter = self._setup_presenter(test_context)
|
|
|
|
obj_mock = test_context.mocker.MagicMock()
|
|
|
|
assert presenter.echo_previous_word(obj_mock) is False
|
|
|
|
presenter.set_word_echo_enabled(True)
|
|
|
|
test_context.patch("cthulhu.ax_text.AXText.get_caret_offset", return_value=5)
|
|
test_context.patch("cthulhu.ax_text.AXText.get_character_count", return_value=10)
|
|
|
|
test_context.patch("cthulhu.ax_text.AXText.get_caret_offset", return_value=0)
|
|
assert presenter.echo_previous_word(obj_mock) is False
|
|
|
|
test_context.patch("cthulhu.ax_text.AXText.get_caret_offset", return_value=5)
|
|
char_mock_1 = test_context.patch("cthulhu.ax_text.AXText.get_character_at_offset")
|
|
char_mock_1.side_effect = [("a", 4, 5), ("b", 3, 4)]
|
|
assert presenter.echo_previous_word(obj_mock) is False
|
|
|
|
char_mock_2 = test_context.patch("cthulhu.ax_text.AXText.get_character_at_offset")
|
|
char_mock_2.side_effect = [(" ", 4, 5), (" ", 3, 4)]
|
|
assert presenter.echo_previous_word(obj_mock) is False
|
|
|
|
char_mock_3 = test_context.patch("cthulhu.ax_text.AXText.get_character_at_offset")
|
|
char_mock_3.side_effect = [(" ", 4, 5), ("a", 3, 4)]
|
|
test_context.patch("cthulhu.ax_text.AXText.get_word_at_offset", return_value=("hello", 0, 5))
|
|
|
|
from cthulhu import presentation_manager
|
|
|
|
speak_text = presentation_manager.get_manager().speak_accessible_text
|
|
assert isinstance(speak_text, Mock)
|
|
speak_text.reset_mock() # pylint: disable=no-member
|
|
|
|
result = presenter.echo_previous_word(obj_mock)
|
|
assert result is True
|
|
speak_text.assert_called_with(obj_mock, "hello") # pylint: disable=no-member
|
|
|
|
char_mock_4 = test_context.patch("cthulhu.ax_text.AXText.get_character_at_offset")
|
|
char_mock_4.side_effect = [(" ", 4, 5), ("a", 3, 4)]
|
|
test_context.patch("cthulhu.ax_text.AXText.get_word_at_offset", return_value=("", 0, 0))
|
|
assert presenter.echo_previous_word(obj_mock) is False
|
|
|
|
def test_echo_previous_sentence(self, test_context: CthulhuTestContext) -> None:
|
|
"""Test echo_previous_sentence method."""
|
|
presenter = self._setup_presenter(test_context)
|
|
|
|
obj_mock = test_context.mocker.MagicMock()
|
|
|
|
assert presenter.echo_previous_sentence(obj_mock) is False
|
|
|
|
presenter.set_sentence_echo_enabled(True)
|
|
|
|
test_context.patch("cthulhu.ax_text.AXText.get_caret_offset", return_value=10)
|
|
|
|
char_mock_1 = test_context.patch("cthulhu.ax_text.AXText.get_character_at_offset")
|
|
char_mock_1.side_effect = [("a", 9, 10), ("b", 8, 9)]
|
|
assert presenter.echo_previous_sentence(obj_mock) is False
|
|
|
|
char_mock_2 = test_context.patch("cthulhu.ax_text.AXText.get_character_at_offset")
|
|
char_mock_2.side_effect = [(" ", 9, 10), (".", 8, 9)]
|
|
test_context.patch(
|
|
"cthulhu.ax_text.AXText.get_sentence_at_offset",
|
|
return_value=("Hello world.", 0, 12),
|
|
)
|
|
|
|
from cthulhu import presentation_manager
|
|
|
|
speak_text = presentation_manager.get_manager().speak_accessible_text
|
|
assert isinstance(speak_text, Mock)
|
|
speak_text.reset_mock() # pylint: disable=no-member
|
|
|
|
result = presenter.echo_previous_sentence(obj_mock)
|
|
assert result is True
|
|
speak_text.assert_called_with(obj_mock, "Hello world.") # pylint: disable=no-member
|
|
|
|
char_mock_3 = test_context.patch("cthulhu.ax_text.AXText.get_character_at_offset")
|
|
char_mock_3.side_effect = [(" ", 9, 10), (".", 8, 9)]
|
|
test_context.patch("cthulhu.ax_text.AXText.get_sentence_at_offset", return_value=("", 0, 0))
|
|
assert presenter.echo_previous_sentence(obj_mock) is False
|
|
|
|
def test_commands_and_bindings(self, test_context: CthulhuTestContext) -> None:
|
|
"""Test commands are registered in CommandManager."""
|
|
presenter = self._setup_presenter(test_context)
|
|
from cthulhu import command_manager
|
|
|
|
# Commands are registered during setup()
|
|
presenter.set_up_commands()
|
|
|
|
# Verify commands are registered in CommandManager
|
|
cmd_manager = command_manager.get_manager()
|
|
assert cmd_manager.get_keyboard_command("cycleKeyEchoHandler") is not None
|
|
|
|
@pytest.mark.parametrize(
|
|
"getter_name,gs_key,_setting_key",
|
|
[
|
|
("get_key_echo_enabled", "key-echo", "enableKeyEcho"),
|
|
("get_character_echo_enabled", "character-echo", "enableEchoByCharacter"),
|
|
("get_word_echo_enabled", "word-echo", "enableEchoByWord"),
|
|
("get_sentence_echo_enabled", "sentence-echo", "enableEchoBySentence"),
|
|
("get_alphabetic_keys_enabled", "alphabetic-keys", "enableAlphabeticKeys"),
|
|
("get_numeric_keys_enabled", "numeric-keys", "enableNumericKeys"),
|
|
("get_punctuation_keys_enabled", "punctuation-keys", "enablePunctuationKeys"),
|
|
("get_space_enabled", "space", "enableSpace"),
|
|
("get_modifier_keys_enabled", "modifier-keys", "enableModifierKeys"),
|
|
("get_function_keys_enabled", "function-keys", "enableFunctionKeys"),
|
|
("get_action_keys_enabled", "action-keys", "enableActionKeys"),
|
|
("get_navigation_keys_enabled", "navigation-keys", "enableNavigationKeys"),
|
|
("get_diacritical_keys_enabled", "diacritical-keys", "enableDiacriticalKeys"),
|
|
],
|
|
)
|
|
def test_getter_returns_dconf_value_when_available(
|
|
self,
|
|
test_context: CthulhuTestContext,
|
|
getter_name: str,
|
|
gs_key: str,
|
|
_setting_key: str,
|
|
) -> None:
|
|
"""Test getter returns dconf value when layered_lookup returns a value."""
|
|
presenter = self._setup_presenter(test_context)
|
|
|
|
from cthulhu import gsettings_registry
|
|
from cthulhu.gsettings_registry import GSettingsSchemaHandle
|
|
|
|
registry = gsettings_registry.get_registry()
|
|
|
|
mock_handle = test_context.Mock(spec=GSettingsSchemaHandle)
|
|
mock_handle.get_boolean.return_value = False
|
|
registry._handles["typing-echo"] = mock_handle
|
|
|
|
getter = getattr(presenter, getter_name)
|
|
assert getter() is False
|
|
mock_handle.get_boolean.assert_called_with(gs_key, "", None)
|
|
|
|
registry._handles.pop("typing-echo", None)
|
|
|
|
@pytest.mark.parametrize(
|
|
"getter_name,setting_key",
|
|
[
|
|
("get_key_echo_enabled", "enableKeyEcho"),
|
|
("get_character_echo_enabled", "enableEchoByCharacter"),
|
|
("get_word_echo_enabled", "enableEchoByWord"),
|
|
("get_sentence_echo_enabled", "enableEchoBySentence"),
|
|
("get_alphabetic_keys_enabled", "enableAlphabeticKeys"),
|
|
("get_numeric_keys_enabled", "enableNumericKeys"),
|
|
("get_punctuation_keys_enabled", "enablePunctuationKeys"),
|
|
("get_space_enabled", "enableSpace"),
|
|
("get_modifier_keys_enabled", "enableModifierKeys"),
|
|
("get_function_keys_enabled", "enableFunctionKeys"),
|
|
("get_action_keys_enabled", "enableActionKeys"),
|
|
("get_navigation_keys_enabled", "enableNavigationKeys"),
|
|
("get_diacritical_keys_enabled", "enableDiacriticalKeys"),
|
|
],
|
|
)
|
|
def test_getter_returns_default_when_disabled(
|
|
self,
|
|
test_context: CthulhuTestContext,
|
|
getter_name: str,
|
|
setting_key: str,
|
|
) -> None:
|
|
"""Test getter returns module-owned default when registry is disabled."""
|
|
presenter = self._setup_presenter(test_context)
|
|
|
|
getter = getattr(presenter, getter_name)
|
|
assert getter() is self._DEFAULT_VALUES[setting_key]
|
|
|
|
def test_get_setting_logs_dconf_layer(self, test_context: CthulhuTestContext) -> None:
|
|
"""Test that dconf lookup logs the source layer and value."""
|
|
presenter = self._setup_presenter(test_context)
|
|
|
|
from cthulhu import debug as debug_mock
|
|
from cthulhu import gsettings_registry
|
|
from cthulhu.gsettings_registry import GSettingsSchemaHandle
|
|
|
|
registry = gsettings_registry.get_registry()
|
|
|
|
mock_handle = test_context.Mock(spec=GSettingsSchemaHandle)
|
|
mock_handle.get_boolean.return_value = False
|
|
registry._handles["typing-echo"] = mock_handle
|
|
|
|
print_msg = debug_mock.print_message
|
|
assert isinstance(print_msg, Mock)
|
|
debug_mock.debugLevel = 800
|
|
print_msg.reset_mock() # pylint: disable=no-member
|
|
|
|
assert presenter.get_key_echo_enabled() is False
|
|
|
|
registry._handles.pop("typing-echo", None)
|