# Unit tests for caret_navigator.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=protected-access # pylint: disable=too-many-arguments # pylint: disable=too-many-positional-arguments # pylint: disable=too-many-locals """Unit tests for caret_navigator.py methods.""" from __future__ import annotations from typing import TYPE_CHECKING, Any import pytest if TYPE_CHECKING: from cthulhu_test_context import CthulhuTestContext @pytest.mark.unit class TestCaretNavigator: """Test CaretNavigator class methods.""" def _setup_dependencies(self, test_context: CthulhuTestContext) -> dict[str, Any]: """Set up mocks for caret_navigator dependencies.""" additional_modules = [ "cthulhu.command_manager", "cthulhu.input_event_manager", "cthulhu.keybindings", "cthulhu.cmdnames", "cthulhu.guilabels", "cthulhu.debug", "cthulhu.ax_object", "cthulhu.ax_text", "cthulhu.script_manager", "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", ] essential_modules = test_context.setup_shared_dependencies(additional_modules) # Set up cmdnames with all required values for structural_navigator cmdnames = essential_modules["cthulhu.cmdnames"] cmdnames.STRUCTURAL_NAVIGATION_MODE_CYCLE = "cycle_mode" cmdnames.BLOCKQUOTE_PREV = "previous_blockquote" cmdnames.BLOCKQUOTE_NEXT = "next_blockquote" cmdnames.BLOCKQUOTE_LIST = "list_blockquotes" cmdnames.BUTTON_PREV = "previous_button" cmdnames.BUTTON_NEXT = "next_button" cmdnames.BUTTON_LIST = "list_buttons" cmdnames.CHECK_BOX_PREV = "previous_checkbox" cmdnames.CHECK_BOX_NEXT = "next_checkbox" cmdnames.CHECK_BOX_LIST = "list_checkboxes" cmdnames.COMBO_BOX_PREV = "previous_combobox" cmdnames.COMBO_BOX_NEXT = "next_combobox" cmdnames.COMBO_BOX_LIST = "list_comboboxes" cmdnames.ENTRY_PREV = "previous_entry" cmdnames.ENTRY_NEXT = "next_entry" cmdnames.ENTRY_LIST = "list_entries" cmdnames.FORM_FIELD_PREV = "previous_form_field" cmdnames.FORM_FIELD_NEXT = "next_form_field" cmdnames.FORM_FIELD_LIST = "list_form_fields" cmdnames.HEADING_PREV = "previous_heading" cmdnames.HEADING_NEXT = "next_heading" cmdnames.HEADING_LIST = "list_headings" cmdnames.HEADING_AT_LEVEL_PREV = "previous_heading_level_%d" cmdnames.HEADING_AT_LEVEL_NEXT = "next_heading_level_%d" cmdnames.HEADING_AT_LEVEL_LIST = "list_headings_level_%d" cmdnames.IFRAME_PREV = "previous_iframe" cmdnames.IFRAME_NEXT = "next_iframe" cmdnames.IFRAME_LIST = "list_iframes" cmdnames.IMAGE_PREV = "previous_image" cmdnames.IMAGE_NEXT = "next_image" cmdnames.IMAGE_LIST = "list_images" cmdnames.LANDMARK_PREV = "previous_landmark" cmdnames.LANDMARK_NEXT = "next_landmark" cmdnames.LANDMARK_LIST = "list_landmarks" cmdnames.LIST_PREV = "previous_list" cmdnames.LIST_NEXT = "next_list" cmdnames.LIST_LIST = "list_lists" cmdnames.LIST_ITEM_PREV = "previous_list_item" cmdnames.LIST_ITEM_NEXT = "next_list_item" cmdnames.LIST_ITEM_LIST = "list_list_items" cmdnames.LIVE_REGION_PREV = "previous_live_region" cmdnames.LIVE_REGION_NEXT = "next_live_region" cmdnames.LIVE_REGION_LAST = "last_live_region" cmdnames.PARAGRAPH_PREV = "previous_paragraph" cmdnames.PARAGRAPH_NEXT = "next_paragraph" cmdnames.PARAGRAPH_LIST = "list_paragraphs" cmdnames.RADIO_BUTTON_PREV = "previous_radio_button" cmdnames.RADIO_BUTTON_NEXT = "next_radio_button" cmdnames.RADIO_BUTTON_LIST = "list_radio_buttons" cmdnames.SEPARATOR_PREV = "previous_separator" cmdnames.SEPARATOR_NEXT = "next_separator" cmdnames.TABLE_PREV = "previous_table" cmdnames.TABLE_NEXT = "next_table" cmdnames.TABLE_LIST = "list_tables" cmdnames.UNVISITED_LINK_PREV = "previous_unvisited_link" cmdnames.UNVISITED_LINK_NEXT = "next_unvisited_link" cmdnames.UNVISITED_LINK_LIST = "list_unvisited_links" cmdnames.VISITED_LINK_PREV = "previous_visited_link" cmdnames.VISITED_LINK_NEXT = "next_visited_link" cmdnames.VISITED_LINK_LIST = "list_visited_links" cmdnames.LINK_PREV = "previous_link" cmdnames.LINK_NEXT = "next_link" cmdnames.LINK_LIST = "list_links" cmdnames.CLICKABLE_PREV = "previous_clickable" cmdnames.CLICKABLE_NEXT = "next_clickable" cmdnames.CLICKABLE_LIST = "list_clickables" cmdnames.LARGE_OBJECT_PREV = "previous_large_object" cmdnames.LARGE_OBJECT_NEXT = "next_large_object" cmdnames.LARGE_OBJECT_LIST = "list_large_objects" cmdnames.CONTAINER_START = "container_start" cmdnames.CONTAINER_END = "container_end" essential_modules["cthulhu.cthulhu_i18n"]._ = lambda x: x essential_modules["cthulhu.debug"].print_message = test_context.Mock() essential_modules["cthulhu.debug"].LEVEL_INFO = 800 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 return essential_modules @pytest.mark.parametrize( "direction,event_provided,context_available,expected_result", [ pytest.param("next", False, True, True, id="next_char_no_event_returns_true"), pytest.param("next", True, False, False, id="next_char_no_context_returns_false"), pytest.param("next", True, True, True, id="next_char_valid_navigation_succeeds"), pytest.param("previous", False, True, True, id="prev_char_no_event_returns_true"), pytest.param("previous", True, False, False, id="prev_char_no_context_returns_false"), pytest.param("previous", True, True, True, id="prev_char_valid_navigation_succeeds"), ], ) def test_character_navigation( self, test_context: CthulhuTestContext, direction: str, event_provided: bool, context_available: bool, expected_result: bool, ) -> None: """Test character navigation (next/previous) with various conditions.""" essential_modules = self._setup_dependencies(test_context) from cthulhu.caret_navigator import CaretNavigator # pylint: disable=import-outside-toplevel ax_object_mock = essential_modules["cthulhu.ax_object"] ax_object_mock.AXObject.supports_text.side_effect = lambda obj: obj is not None ax_object_mock.AXObject.is_valid.side_effect = lambda obj: obj is not None ax_object_mock.AXObject.is_ancestor.side_effect = ( lambda obj, root, same: obj is not None and root is not None ) navigator = CaretNavigator() test_context.patch_object(navigator, "_get_root_object", return_value=None) mock_script = test_context.Mock() mock_event = test_context.Mock() if event_provided else None if context_available: mock_obj = test_context.Mock() if direction == "next": mock_script.utilities.next_context.return_value = (mock_obj, 10) else: mock_script.utilities.previous_context.return_value = (mock_obj, 5) elif direction == "next": mock_script.utilities.next_context.return_value = (None, 0) else: mock_script.utilities.previous_context.return_value = (None, 0) navigation_method = getattr(navigator, f"{direction}_character") result = navigation_method(mock_script, mock_event) assert result == expected_result if expected_result: pres_manager = essential_modules["cthulhu.presentation_manager"].get_manager() assert navigator._last_input_event == mock_event mock_script.utilities.set_caret_position.assert_called_once() pres_manager.interrupt_presentation.assert_called_once() mock_script.update_braille.assert_called_once() mock_script.say_character.assert_called_once() @pytest.mark.parametrize( "direction,context_result,word_contents,expected_result", [ pytest.param("next", (None, 0), None, False, id="next_word_no_context"), pytest.param("next", ("obj", 20), [], False, id="next_word_no_contents"), pytest.param( "next", ("obj", 20), [("obj", 20, 25, "word")], True, id="next_word_success", ), pytest.param("previous", (None, 0), None, False, id="previous_word_no_context"), pytest.param("previous", ("obj", 15), [], False, id="previous_word_no_contents"), pytest.param( "previous", ("obj", 15), [("obj", 10, 15, "word")], True, id="previous_word_success", ), ], ) def test_word_navigation( self, test_context: CthulhuTestContext, direction: str, context_result: tuple, word_contents: list | None, expected_result: bool, ) -> None: """Test word navigation (next/previous) with various error conditions.""" essential_modules = self._setup_dependencies(test_context) from cthulhu.caret_navigator import CaretNavigator # pylint: disable=import-outside-toplevel navigator = CaretNavigator() mock_script = test_context.Mock() mock_event = test_context.Mock() if direction == "next": mock_script.utilities.next_context.return_value = context_result else: mock_script.utilities.previous_context.return_value = context_result mock_script.utilities.get_word_contents_at_offset.return_value = word_contents or [] mock_script.utilities.set_caret_position = test_context.Mock() mock_script.update_braille = test_context.Mock() mock_script.say_word = test_context.Mock() navigation_method = getattr(navigator, f"{direction}_word") result = navigation_method(mock_script, mock_event) assert result == expected_result pres_manager = essential_modules["cthulhu.presentation_manager"].get_manager() if expected_result: assert navigator._last_input_event == mock_event mock_script.utilities.set_caret_position.assert_called() pres_manager.interrupt_presentation.assert_called_once() mock_script.update_braille.assert_called_once() mock_script.say_word.assert_called_once() else: mock_script.utilities.set_caret_position.assert_not_called() pres_manager.interrupt_presentation.assert_not_called() @pytest.mark.parametrize( "test_method,expected_result", [ pytest.param("suspend_commands", True, id="suspend_commands"), pytest.param("toggle_enabled", True, id="toggle_enabled"), ], ) def test_navigator_control_methods( self, test_context: CthulhuTestContext, test_method: str, expected_result: bool, ) -> None: """Test CaretNavigator control methods.""" essential_modules = self._setup_dependencies(test_context) from cthulhu.caret_navigator import CaretNavigator # pylint: disable=import-outside-toplevel navigator = CaretNavigator() mock_script = test_context.Mock() mock_event = test_context.Mock() if test_method == "suspend_commands": mock_cmd_mgr = test_context.Mock() essential_modules["cthulhu.command_manager"].get_manager.return_value = mock_cmd_mgr test_context.patch_object(navigator, "_is_active_script", return_value=True) navigator._suspended = False navigator.suspend_commands(mock_script, True, "test reason") assert navigator._suspended == expected_result mock_cmd_mgr.set_group_suspended.assert_called_once() elif test_method == "toggle_enabled": mock_cmd_mgr = test_context.Mock() essential_modules["cthulhu.command_manager"].get_manager.return_value = mock_cmd_mgr guilabels_mock = essential_modules["cthulhu.guilabels"] guilabels_mock.CARET_NAVIGATION_ENABLED = "Caret navigation enabled" guilabels_mock.CARET_NAVIGATION_DISABLED = "Caret navigation disabled" result = navigator.toggle_enabled(mock_script, mock_event) assert result == expected_result mock_cmd_mgr.set_group_enabled.assert_called_once() def test_navigator_initialization(self, test_context: CthulhuTestContext) -> None: """Test CaretNavigator initialization.""" self._setup_dependencies(test_context) from cthulhu import command_manager from cthulhu.caret_navigator import CaretNavigator navigator = CaretNavigator() assert hasattr(navigator, "_last_input_event") assert hasattr(navigator, "_suspended") assert navigator._last_input_event is None assert navigator._suspended is False # Commands are registered in CommandManager cmd_manager = command_manager.get_manager() assert cmd_manager is not None @pytest.mark.parametrize( "navigation_type,in_say_all,current_line,next_prev_contents,expected_result", [ pytest.param( "next_line", True, [("obj", 0, 10, "text")], [], True, id="next_line_in_say_all", ), pytest.param("next_line", False, [], [], False, id="next_line_no_current_line"), pytest.param( "next_line", False, [("obj", 0, 10, "text")], [], False, id="next_line_no_next_contents", ), pytest.param( "next_line", False, [("obj", 0, 10, "text")], [("obj2", 11, 21, "next")], True, id="next_line_success", ), pytest.param( "previous_line", True, [("obj", 0, 10, "text")], [], True, id="previous_line_in_say_all", ), pytest.param("previous_line", False, [], [], False, id="previous_line_no_contents"), pytest.param( "previous_line", False, [("obj", 0, 10, "text")], [("obj", 0, 10, "prev")], True, id="previous_line_success", ), ], ) def test_line_navigation( self, test_context: CthulhuTestContext, navigation_type: str, in_say_all: bool, current_line: list | None, next_prev_contents: list, expected_result: bool, ) -> None: """Test line navigation including say-all mode handling.""" essential_modules = self._setup_dependencies(test_context) from cthulhu.caret_navigator import CaretNavigator # pylint: disable=import-outside-toplevel navigator = CaretNavigator() mock_script = test_context.Mock() mock_event = test_context.Mock() focus_manager_mock = essential_modules["cthulhu.focus_manager"] manager_instance = test_context.Mock() focus_manager_mock.get_manager.return_value = manager_instance manager_instance.in_say_all.return_value = in_say_all if in_say_all: from cthulhu import say_all_presenter # pylint: disable=import-outside-toplevel say_all_presenter.get_presenter().set_rewind_and_fast_forward_enabled(True) if navigation_type == "next_line" and not in_say_all: mock_script.utilities.get_caret_context.return_value = ("obj", 5) mock_script.utilities.get_line_contents_at_offset.return_value = current_line mock_script.utilities.get_next_line_contents.return_value = next_prev_contents test_context.patch_object(navigator, "_get_end_of_file", return_value=(None, -1)) test_context.patch_object(navigator, "_line_contains_context", return_value=False) test_context.patch_object(navigator, "_is_navigable_object", return_value=True) elif navigation_type == "previous_line" and not in_say_all: mock_script.utilities.get_caret_context.return_value = ("obj", 5) mock_script.utilities.get_line_contents_at_offset.return_value = current_line mock_script.utilities.get_previous_line_contents.return_value = next_prev_contents test_context.patch_object(navigator, "_get_start_of_file", return_value=(None, -1)) test_context.patch_object(navigator, "_line_contains_context", return_value=False) test_context.patch_object(navigator, "_is_navigable_object", return_value=True) mock_script.utilities.set_caret_position = test_context.Mock() pres_manager = essential_modules["cthulhu.presentation_manager"].get_manager() pres_manager.interrupt_presentation.reset_mock() pres_manager.speak_contents.reset_mock() pres_manager.display_contents.reset_mock() navigation_method = getattr(navigator, f"{navigation_type}") result = navigation_method(mock_script, mock_event) assert result == expected_result if expected_result and not in_say_all: assert navigator._last_input_event == mock_event mock_script.utilities.set_caret_position.assert_called() pres_manager.interrupt_presentation.assert_called_once() pres_manager.speak_contents.assert_called_once() pres_manager.display_contents.assert_called_once() elif in_say_all: assert navigator._last_input_event != mock_event @pytest.mark.parametrize( "navigation_type,line_contents,expected_result", [ pytest.param("start_of_line", [], False, id="start_of_line_no_line"), pytest.param( "start_of_line", [("obj", 5, 15, "text")], True, id="start_of_line_success", ), pytest.param("end_of_line", [], False, id="end_of_line_no_line"), pytest.param( "end_of_line", [("obj", 5, 15, "text ")], True, id="end_of_line_with_space", ), pytest.param("end_of_line", [("obj", 5, 15, "text")], True, id="end_of_line_no_space"), ], ) def test_line_boundary_navigation( self, test_context: CthulhuTestContext, navigation_type: str, line_contents: list, expected_result: bool, ) -> None: """Test start/end of line navigation.""" essential_modules = self._setup_dependencies(test_context) from cthulhu.caret_navigator import CaretNavigator # pylint: disable=import-outside-toplevel navigator = CaretNavigator() mock_script = test_context.Mock() mock_event = test_context.Mock() mock_script.utilities.get_caret_context.return_value = ("obj", 10) mock_script.utilities.get_line_contents_at_offset.return_value = line_contents mock_script.utilities.set_caret_position = test_context.Mock() mock_script.say_character = test_context.Mock() pres_manager = essential_modules["cthulhu.presentation_manager"].get_manager() pres_manager.interrupt_presentation.reset_mock() pres_manager.display_contents.reset_mock() navigation_method = getattr(navigator, f"{navigation_type}") result = navigation_method(mock_script, mock_event) assert result == expected_result if expected_result: assert navigator._last_input_event == mock_event mock_script.utilities.set_caret_position.assert_called() pres_manager.interrupt_presentation.assert_called_once() mock_script.say_character.assert_called_once() pres_manager.display_contents.assert_called_once() @pytest.mark.parametrize( "script_is_active,expected_result", [ pytest.param(True, True, id="script_is_active"), pytest.param(False, False, id="script_is_not_active"), ], ) def test_is_active_script( self, test_context: CthulhuTestContext, script_is_active: bool, expected_result: bool, ) -> None: """Test _is_active_script method with active and non-active scripts.""" essential_modules = self._setup_dependencies(test_context) from cthulhu.caret_navigator import CaretNavigator # pylint: disable=import-outside-toplevel navigator = CaretNavigator() mock_script = test_context.Mock() mock_active_script = test_context.Mock() script_manager_mock = essential_modules["cthulhu.script_manager"] manager_instance = test_context.Mock() script_manager_mock.get_manager.return_value = manager_instance if script_is_active: manager_instance.get_active_script.return_value = mock_script else: manager_instance.get_active_script.return_value = mock_active_script debug_mock = essential_modules["cthulhu.debug"] debug_mock.LEVEL_INFO = 800 debug_mock.print_tokens = test_context.Mock() result = navigator._is_active_script(mock_script) assert result == expected_result if not script_is_active: debug_mock.print_tokens.assert_called_once() def test_get_is_enabled(self, test_context: CthulhuTestContext) -> None: """Test get_is_enabled returns setting value.""" self._setup_dependencies(test_context) from cthulhu.caret_navigator import CaretNavigator # pylint: disable=import-outside-toplevel navigator = CaretNavigator() result = navigator.get_is_enabled() assert result is True def test_set_is_enabled_no_change(self, test_context: CthulhuTestContext) -> None: """Test set_is_enabled still calls set_group_enabled even if value unchanged.""" essential_modules = self._setup_dependencies(test_context) mock_cmd_mgr = test_context.Mock() essential_modules["cthulhu.command_manager"].get_manager.return_value = mock_cmd_mgr from cthulhu.caret_navigator import CaretNavigator # pylint: disable=import-outside-toplevel navigator = CaretNavigator() result = navigator.set_is_enabled(True) assert result is True mock_cmd_mgr.set_group_enabled.assert_called_once() def test_set_is_enabled_updates_setting(self, test_context: CthulhuTestContext) -> None: """Test set_is_enabled updates setting and calls CommandManager.""" essential_modules = self._setup_dependencies(test_context) from cthulhu import gsettings_registry gsettings_registry.get_registry().set_runtime_value("caret-navigation", "enabled", False) mock_script = test_context.Mock() essential_modules[ "cthulhu.script_manager" ].get_manager.return_value.get_active_script.return_value = mock_script mock_cmd_mgr = test_context.Mock() essential_modules["cthulhu.command_manager"].get_manager.return_value = mock_cmd_mgr from cthulhu.caret_navigator import CaretNavigator # pylint: disable=import-outside-toplevel navigator = CaretNavigator() result = navigator.set_is_enabled(True) assert result is True assert navigator.get_is_enabled() is True assert navigator._last_input_event is None mock_cmd_mgr.set_group_enabled.assert_called_once() def test_set_is_enabled_no_active_script(self, test_context: CthulhuTestContext) -> None: """Test set_is_enabled updates state even with no active script.""" essential_modules = self._setup_dependencies(test_context) from cthulhu import gsettings_registry gsettings_registry.get_registry().set_runtime_value("caret-navigation", "enabled", False) essential_modules[ "cthulhu.script_manager" ].get_manager.return_value.get_active_script.return_value = None mock_cmd_mgr = test_context.Mock() essential_modules["cthulhu.command_manager"].get_manager.return_value = mock_cmd_mgr from cthulhu.caret_navigator import CaretNavigator # pylint: disable=import-outside-toplevel navigator = CaretNavigator() result = navigator.set_is_enabled(True) assert result is True mock_cmd_mgr.set_group_enabled.assert_called_once() def test_get_triggers_focus_mode(self, test_context: CthulhuTestContext) -> None: """Test get_triggers_focus_mode returns setting value.""" self._setup_dependencies(test_context) from cthulhu.caret_navigator import CaretNavigator # pylint: disable=import-outside-toplevel navigator = CaretNavigator() result = navigator.get_triggers_focus_mode() assert result is False def test_set_triggers_focus_mode(self, test_context: CthulhuTestContext) -> None: """Test set_triggers_focus_mode updates setting.""" self._setup_dependencies(test_context) from cthulhu import gsettings_registry gsettings_registry.get_registry().set_runtime_value( "caret-navigation", "triggers-focus-mode", True, ) from cthulhu.caret_navigator import CaretNavigator # pylint: disable=import-outside-toplevel navigator = CaretNavigator() result = navigator.set_triggers_focus_mode(False) assert result is True assert navigator.get_triggers_focus_mode() is False def test_set_triggers_focus_mode_no_change(self, test_context: CthulhuTestContext) -> None: """Test set_triggers_focus_mode returns early if unchanged.""" self._setup_dependencies(test_context) from cthulhu import gsettings_registry gsettings_registry.get_registry().set_runtime_value( "caret-navigation", "triggers-focus-mode", True, ) from cthulhu.caret_navigator import CaretNavigator # pylint: disable=import-outside-toplevel navigator = CaretNavigator() result = navigator.set_triggers_focus_mode(True) assert result is True # set_setting no longer used - settings are set directly def test_get_enabled_for_script(self, test_context: CthulhuTestContext) -> None: """Test get_enabled_for_script returns script-specific state.""" self._setup_dependencies(test_context) mock_script = test_context.Mock() from cthulhu.caret_navigator import CaretNavigator # pylint: disable=import-outside-toplevel navigator = CaretNavigator() navigator._enabled_for_script[mock_script] = True result = navigator.get_enabled_for_script(mock_script) assert result is True def test_get_enabled_for_script_default(self, test_context: CthulhuTestContext) -> None: """Test get_enabled_for_script returns False by default.""" self._setup_dependencies(test_context) mock_script = test_context.Mock() from cthulhu.caret_navigator import CaretNavigator # pylint: disable=import-outside-toplevel navigator = CaretNavigator() result = navigator.get_enabled_for_script(mock_script) assert result is False def test_set_enabled_for_script(self, test_context: CthulhuTestContext) -> None: """Test set_enabled_for_script updates script-specific state and calls set_is_enabled.""" essential_modules = self._setup_dependencies(test_context) mock_script = test_context.Mock() from cthulhu import gsettings_registry gsettings_registry.get_registry().set_runtime_value("caret-navigation", "enabled", False) essential_modules[ "cthulhu.script_manager" ].get_manager.return_value.get_active_script.return_value = mock_script mock_cmd_mgr = test_context.Mock() essential_modules["cthulhu.command_manager"].get_manager.return_value = mock_cmd_mgr from cthulhu.caret_navigator import CaretNavigator # pylint: disable=import-outside-toplevel navigator = CaretNavigator() test_context.patch_object(navigator, "_is_active_script", return_value=True) navigator.set_enabled_for_script(mock_script, True) assert navigator._enabled_for_script[mock_script] is True mock_cmd_mgr.set_group_enabled.assert_called_once() def test_set_enabled_for_script_inactive_script(self, test_context: CthulhuTestContext) -> None: """Test set_enabled_for_script doesn't call set_group_enabled for inactive script.""" essential_modules = self._setup_dependencies(test_context) mock_script = test_context.Mock() mock_cmd_mgr = test_context.Mock() essential_modules["cthulhu.command_manager"].get_manager.return_value = mock_cmd_mgr from cthulhu.caret_navigator import CaretNavigator # pylint: disable=import-outside-toplevel navigator = CaretNavigator() test_context.patch_object(navigator, "_is_active_script", return_value=False) navigator.set_enabled_for_script(mock_script, True) assert navigator._enabled_for_script[mock_script] is True mock_cmd_mgr.set_group_enabled.assert_not_called() def test_set_enabled_for_script_always_calls_set_group_enabled( self, test_context: CthulhuTestContext, ) -> None: """Test set_enabled_for_script always calls set_group_enabled even if setting matches. This is a regression test for issue #655 where caret navigation commands were not being enabled because set_is_enabled() would early-return when the setting already matched the desired value. """ essential_modules = self._setup_dependencies(test_context) mock_script = test_context.Mock() essential_modules[ "cthulhu.script_manager" ].get_manager.return_value.get_active_script.return_value = mock_script mock_cmd_mgr = test_context.Mock() essential_modules["cthulhu.command_manager"].get_manager.return_value = mock_cmd_mgr from cthulhu.caret_navigator import CaretNavigator # pylint: disable=import-outside-toplevel navigator = CaretNavigator() test_context.patch_object(navigator, "_is_active_script", return_value=True) navigator.set_enabled_for_script(mock_script, True) assert navigator._enabled_for_script[mock_script] is True mock_cmd_mgr.set_group_enabled.assert_called_once() def test_last_command_prevents_focus_mode_true(self, test_context: CthulhuTestContext) -> None: """Test last_command_prevents_focus_mode returns True.""" self._setup_dependencies(test_context) from cthulhu.caret_navigator import CaretNavigator # pylint: disable=import-outside-toplevel navigator = CaretNavigator() mock_event = test_context.Mock() navigator._last_input_event = mock_event test_context.patch_object( navigator, "last_input_event_was_navigation_command", return_value=True, ) result = navigator.last_command_prevents_focus_mode() assert result is True def test_last_command_prevents_focus_mode_false_no_event( self, test_context: CthulhuTestContext, ) -> None: """Test last_command_prevents_focus_mode returns False if no event.""" self._setup_dependencies(test_context) from cthulhu.caret_navigator import CaretNavigator # pylint: disable=import-outside-toplevel navigator = CaretNavigator() navigator._last_input_event = None result = navigator.last_command_prevents_focus_mode() assert result is False def test_last_command_prevents_focus_mode_false_setting_true( self, test_context: CthulhuTestContext, ) -> None: """Test last_command_prevents_focus_mode returns False if setting True.""" self._setup_dependencies(test_context) from cthulhu.caret_navigator import CaretNavigator # pylint: disable=import-outside-toplevel navigator = CaretNavigator() navigator.set_triggers_focus_mode(True) mock_event = test_context.Mock() navigator._last_input_event = mock_event test_context.patch_object( navigator, "last_input_event_was_navigation_command", return_value=True, ) result = navigator.last_command_prevents_focus_mode() assert result is False def test_successful_navigation_emits_region_changed( self, test_context: CthulhuTestContext, ) -> None: """Test successful caret navigation emits region_changed with CARET_NAVIGATOR mode.""" essential_modules = self._setup_dependencies(test_context) from cthulhu import focus_manager from cthulhu.caret_navigator import CaretNavigator ax_object_mock = essential_modules["cthulhu.ax_object"] ax_object_mock.AXObject.supports_text.side_effect = lambda obj: obj is not None ax_object_mock.AXObject.is_valid.side_effect = lambda obj: obj is not None ax_object_mock.AXObject.is_ancestor.side_effect = ( lambda obj, root, same: obj is not None and root is not None ) focus_manager_mock = essential_modules["cthulhu.focus_manager"] manager_instance = test_context.Mock() focus_manager_mock.get_manager.return_value = manager_instance focus_manager_mock.CARET_NAVIGATOR = focus_manager.CARET_NAVIGATOR navigator = CaretNavigator() test_context.patch_object(navigator, "_get_root_object", return_value=None) mock_script = test_context.Mock() mock_event = test_context.Mock() mock_obj = test_context.Mock() mock_script.utilities.next_context.return_value = (mock_obj, 10) result = navigator.next_character(mock_script, mock_event) assert result is True manager_instance.emit_region_changed.assert_called() call_kwargs = manager_instance.emit_region_changed.call_args assert call_kwargs.kwargs.get("mode") == focus_manager.CARET_NAVIGATOR