Files
cthulhu/tests/test_document_selection_regressions.py
2026-07-19 15:22:32 -04:00

230 lines
8.5 KiB
Python

import sys
import types
import unittest
from contextlib import ExitStack
from pathlib import Path
from unittest import mock
import gi
gi.require_version("Gdk", "3.0")
gi.require_version("Gtk", "3.0")
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
sys.modules.setdefault(
"cthulhu.cthulhu_i18n",
types.SimpleNamespace(
C_=lambda _context, message: message,
_=lambda message: message,
ngettext=lambda singular, plural, count: singular if count == 1 else plural,
),
)
sys.modules.setdefault(
"cthulhu.cthulhu_platform",
types.SimpleNamespace(
version="test",
revision="",
tablesdir="",
datadir="",
),
)
from cthulhu import ax_document_selection
from cthulhu.script_utilities import Utilities
class FakeDocument:
def __init__(self):
self.currentSelections = []
self.setCalls = []
class FakeTextObject:
def __init__(self, name, text, document):
self.name = name
self.text = text
self.document = document
self.next_obj = None
self.prev_obj = None
self.caret_offset = 0
def __repr__(self):
return f"<FakeTextObject {self.name}>"
class DocumentSelectionRegressionTests(unittest.TestCase):
def setUp(self):
self.document = FakeDocument()
self.first = FakeTextObject("first", "alpha", self.document)
self.second = FakeTextObject("second", "beta", self.document)
self.first.next_obj = self.second
self.second.prev_obj = self.first
self.script = types.SimpleNamespace(
pointOfReference={},
sayPhrase=mock.Mock(),
speakMessage=mock.Mock(),
)
self.utility = object.__new__(Utilities)
self.utility._script = self.script
self.utility._clipboardHandlerId = None
self.utility.expandEOCs = lambda obj, start=0, end=-1: obj.text[start:len(obj.text) if end == -1 else end]
self.utility.queryNonEmptyText = lambda obj: obj if getattr(obj, "text", "") else None
self.utility.findNextObject = lambda obj: getattr(obj, "next_obj", None)
self.utility.findPreviousObject = lambda obj: getattr(obj, "prev_obj", None)
self.utility.getDocumentForObject = lambda obj: self.document if isinstance(obj, FakeTextObject) else obj if obj is self.document else None
self.utility.isSpreadSheetCell = lambda obj: False
@staticmethod
def _make_selection(start_object, start_offset, end_object, end_offset, start_is_active=False):
return types.SimpleNamespace(
start_object=start_object,
start_offset=start_offset,
end_object=end_object,
end_offset=end_offset,
start_is_active=start_is_active,
)
@staticmethod
def _new_text_selection():
return types.SimpleNamespace(
start_object=None,
start_offset=0,
end_object=None,
end_offset=0,
start_is_active=False,
)
def _patch_environment(self, local_ranges=None):
local_ranges = local_ranges or {}
remove_selection = mock.Mock()
set_selected_text = mock.Mock()
def get_selected_ranges(obj):
return list(local_ranges.get(obj, []))
def get_character_count(obj):
return len(obj.text)
def get_caret_offset(obj):
return obj.caret_offset
def get_substring(obj, start, end):
if end == -1:
end = len(obj.text)
return obj.text[start:end]
def get_text_selections(document):
return list(document.currentSelections)
def set_text_selections(document, selections):
document.setCalls.append(list(selections))
document.currentSelections = list(selections)
return True
stack = ExitStack()
stack.enter_context(mock.patch("cthulhu.script_utilities.AXObject.supports_text", side_effect=lambda obj: isinstance(obj, FakeTextObject)))
stack.enter_context(mock.patch("cthulhu.script_utilities.AXText.get_selected_ranges", side_effect=get_selected_ranges))
stack.enter_context(mock.patch("cthulhu.script_utilities.AXText.get_character_count", side_effect=get_character_count))
stack.enter_context(mock.patch("cthulhu.script_utilities.AXText.get_caret_offset", side_effect=get_caret_offset))
stack.enter_context(mock.patch("cthulhu.script_utilities.AXText.get_substring", side_effect=get_substring))
stack.enter_context(mock.patch("cthulhu.script_utilities.AXText.set_selected_text", set_selected_text))
stack.enter_context(mock.patch("cthulhu.script_utilities.AXText._get_n_selections", return_value=1))
stack.enter_context(mock.patch("cthulhu.script_utilities.AXText._remove_selection", remove_selection))
fakeAtspi = types.SimpleNamespace(
Document=types.SimpleNamespace(
get_text_selections=mock.Mock(side_effect=get_text_selections),
set_text_selections=mock.Mock(side_effect=set_text_selections),
),
TextSelection=mock.Mock(side_effect=self._new_text_selection),
)
stack.enter_context(mock.patch.object(ax_document_selection, "Atspi", fakeAtspi))
stack.enter_context(
mock.patch(
"cthulhu.script_utilities.cthulhu.cthulhuApp",
new=types.SimpleNamespace(
settingsManager=types.SimpleNamespace(
getSetting=lambda *_args, **_kwargs: False,
)
),
create=True,
)
)
self.addCleanup(stack.close)
return remove_selection, set_selected_text
def test_selected_text_uses_document_selection_when_local_ranges_are_empty(self):
self.document.currentSelections = [
self._make_selection(self.first, 2, self.second, 2),
]
self._patch_environment()
self.assertEqual(self.utility.selectedText(self.first), ["pha", 2, 5])
self.assertEqual(self.utility.selectedText(self.second), ["be", 0, 2])
def test_all_text_selections_project_document_ranges_onto_each_object(self):
self.document.currentSelections = [
self._make_selection(self.first, 2, self.second, 2),
]
self._patch_environment()
self.assertEqual(self.utility.allTextSelections(self.first), [(2, 5)])
self.assertEqual(self.utility.allTextSelections(self.second), [(0, 2)])
def test_all_selected_text_aggregates_multi_object_document_selection(self):
self.document.currentSelections = [
self._make_selection(self.first, 2, self.second, 2),
]
self._patch_environment()
self.assertEqual(self.utility.allSelectedText(self.first), ("pha be", 2, 5))
def test_clear_text_selection_uses_document_api_when_selection_is_authoritative(self):
self.document.currentSelections = [
self._make_selection(self.first, 1, self.second, 3),
]
remove_selection, _ = self._patch_environment()
self.utility.clearTextSelection(self.first)
self.assertEqual(self.document.setCalls, [[]])
remove_selection.assert_not_called()
def test_adjust_text_selection_preserves_other_document_ranges(self):
self.document.currentSelections = [
self._make_selection(self.first, 2, self.first, 4),
self._make_selection(self.second, 1, self.second, 3),
]
self.first.caret_offset = 4
_, set_selected_text = self._patch_environment()
self.utility.adjustTextSelection(self.first, 5)
self.assertEqual(len(self.document.setCalls), 1)
stored = [
(selection.start_object, selection.start_offset,
selection.end_object, selection.end_offset,
selection.start_is_active)
for selection in self.document.setCalls[0]
]
self.assertEqual(
stored,
[
(self.first, 2, self.first, 5, False),
(self.second, 1, self.second, 3, False),
],
)
set_selected_text.assert_not_called()
def test_selected_text_falls_back_to_raw_substring_when_expand_eocs_is_empty(self):
self.document.currentSelections = []
self.utility.expandEOCs = lambda _obj, _start=0, _end=-1: ""
self._patch_environment(local_ranges={self.first: [(2, 5)]})
self.assertEqual(self.utility.selectedText(self.first), ["pha", 2, 5])
if __name__ == "__main__":
unittest.main()