Add the document selection adapter, integrate it through script utilities and major callers, and package the clipboard fallback work that was needed during manual testing on Wayland. Also include a handoff note for the still-open browser link-selection issue so other developers can continue from the current branch state without reconstructing the debug trail.
224 lines
8.4 KiB
Python
224 lines
8.4 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.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))
|
|
stack.enter_context(mock.patch("cthulhu.script_utilities.Atspi.Document.get_text_selections", side_effect=get_text_selections))
|
|
stack.enter_context(mock.patch("cthulhu.script_utilities.Atspi.Document.set_text_selections", side_effect=set_text_selections))
|
|
stack.enter_context(mock.patch("cthulhu.script_utilities.Atspi.TextSelection", side_effect=self._new_text_selection))
|
|
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()
|