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.
473 lines
19 KiB
Python
473 lines
19 KiB
Python
import sys
|
|
import unittest
|
|
from pathlib import Path
|
|
from unittest import mock
|
|
|
|
import gi
|
|
|
|
gi.require_version("Atspi", "2.0")
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
|
|
|
from gi.repository import Atspi
|
|
|
|
from cthulhu import messages
|
|
from cthulhu.scripts import default
|
|
|
|
|
|
CONTROL_MASK = 1 << Atspi.ModifierType.CONTROL
|
|
|
|
|
|
class FakeKeyboardEvent:
|
|
def __init__(self, pressed, keyval_name, modifiers, obj, event_string=None):
|
|
self._pressed = pressed
|
|
self.keyval_name = keyval_name
|
|
self.modifiers = modifiers
|
|
self._obj = obj
|
|
self.event_string = keyval_name if event_string is None else event_string
|
|
|
|
def get_object(self):
|
|
return self._obj
|
|
|
|
def is_pressed_key(self):
|
|
return self._pressed
|
|
|
|
|
|
def run_repeating_timeout(_delay, callback, generation, max_checks=10):
|
|
for _ in range(max_checks):
|
|
if not callback(generation):
|
|
break
|
|
|
|
|
|
class DefaultScriptClipboardRegressionTests(unittest.TestCase):
|
|
def _make_script(self):
|
|
script = object.__new__(default.Script)
|
|
script.utilities = mock.Mock()
|
|
script.presentMessage = mock.Mock()
|
|
script.flatReviewPresenter = mock.Mock()
|
|
script.flatReviewPresenter.is_active.return_value = False
|
|
script._pendingKeyboardClipboardCommand = None
|
|
script._pendingKeyboardClipboardRelease = None
|
|
script._keyboardClipboardCommandGeneration = 0
|
|
script._sessionType = "x11"
|
|
return script
|
|
|
|
@mock.patch("cthulhu.scripts.default.AXUtilities.is_terminal", return_value=False)
|
|
@mock.patch(
|
|
"cthulhu.scripts.default.GLib.timeout_add",
|
|
side_effect=lambda _delay, callback, generation: callback(generation),
|
|
)
|
|
def test_keyboard_copy_fallback_presents_message_when_callback_never_arrives(
|
|
self,
|
|
_timeout_add,
|
|
_is_terminal,
|
|
):
|
|
script = self._make_script()
|
|
source = object()
|
|
script.utilities.getClipboardContents.side_effect = ["before", "copied text"]
|
|
script.utilities.objectContentsAreInClipboard.return_value = True
|
|
|
|
script.updateKeyboardEventState(FakeKeyboardEvent(True, "c", CONTROL_MASK, source), None)
|
|
script.updateKeyboardEventState(FakeKeyboardEvent(False, "c", CONTROL_MASK, source), None)
|
|
|
|
script.utilities.objectContentsAreInClipboard.assert_called_once_with(source)
|
|
script.presentMessage.assert_called_once_with(
|
|
messages.CLIPBOARD_COPIED_FULL,
|
|
messages.CLIPBOARD_COPIED_BRIEF,
|
|
)
|
|
|
|
@mock.patch("cthulhu.scripts.default.AXUtilities.is_terminal", return_value=False)
|
|
@mock.patch(
|
|
"cthulhu.scripts.default.GLib.timeout_add",
|
|
side_effect=run_repeating_timeout,
|
|
)
|
|
def test_keyboard_copy_fallback_uses_captured_selection_when_clipboard_stays_empty(
|
|
self,
|
|
_timeout_add,
|
|
_is_terminal,
|
|
):
|
|
script = self._make_script()
|
|
source = object()
|
|
script.utilities.getClipboardContents.side_effect = ["before"] + [""] * 10
|
|
script.utilities.allSelectedText.return_value = ("The quick brown", 0, 15)
|
|
script.utilities.selectedText.return_value = ("The quick brown", 0, 15)
|
|
script.utilities.objectContentsAreInClipboard.return_value = False
|
|
script.utilities.isInActiveApp.return_value = False
|
|
|
|
script.updateKeyboardEventState(FakeKeyboardEvent(True, "c", CONTROL_MASK, source), None)
|
|
script.updateKeyboardEventState(FakeKeyboardEvent(False, "c", CONTROL_MASK, source), None)
|
|
|
|
script.presentMessage.assert_called_once_with(
|
|
messages.CLIPBOARD_COPIED_FULL,
|
|
messages.CLIPBOARD_COPIED_BRIEF,
|
|
)
|
|
|
|
@mock.patch("cthulhu.scripts.default.AXUtilities.is_terminal", return_value=False)
|
|
@mock.patch(
|
|
"cthulhu.scripts.default.GLib.timeout_add",
|
|
side_effect=lambda _delay, callback, generation: callback(generation),
|
|
)
|
|
def test_keyboard_copy_fallback_handles_release_before_press_order(
|
|
self,
|
|
_timeout_add,
|
|
_is_terminal,
|
|
):
|
|
script = self._make_script()
|
|
source = object()
|
|
script.utilities.getClipboardContents.side_effect = ["before", ""]
|
|
script.utilities.allSelectedText.return_value = ("3086180", 0, 7)
|
|
script.utilities.selectedText.return_value = ("3086180", 0, 7)
|
|
script.utilities.objectContentsAreInClipboard.return_value = False
|
|
script.utilities.isInActiveApp.return_value = False
|
|
|
|
script.updateKeyboardEventState(
|
|
FakeKeyboardEvent(False, "0x3", CONTROL_MASK, source, "\x03"),
|
|
None,
|
|
)
|
|
script.updateKeyboardEventState(
|
|
FakeKeyboardEvent(True, "0x3", CONTROL_MASK, source, "\x03"),
|
|
None,
|
|
)
|
|
|
|
script.presentMessage.assert_called_once_with(
|
|
messages.CLIPBOARD_COPIED_FULL,
|
|
messages.CLIPBOARD_COPIED_BRIEF,
|
|
)
|
|
|
|
@mock.patch("cthulhu.scripts.default.AXUtilities.is_terminal", return_value=False)
|
|
@mock.patch("cthulhu.scripts.default.GLib.idle_add")
|
|
@mock.patch("cthulhu.scripts.default.GLib.timeout_add")
|
|
def test_wayland_keyboard_copy_is_presented_immediately(
|
|
self,
|
|
timeout_add,
|
|
idle_add,
|
|
_is_terminal,
|
|
):
|
|
script = self._make_script()
|
|
script._sessionType = "wayland"
|
|
source = object()
|
|
script.utilities.getClipboardContents.return_value = ""
|
|
script.utilities.allSelectedText.return_value = ("The quick brown", 0, 15)
|
|
script.utilities.selectedText.return_value = ("The quick brown", 0, 15)
|
|
script.utilities.objectContentsAreInClipboard.return_value = False
|
|
script.utilities.isInActiveApp.return_value = False
|
|
|
|
script.updateKeyboardEventState(FakeKeyboardEvent(True, "c", CONTROL_MASK, source), None)
|
|
|
|
timeout_add.assert_not_called()
|
|
idle_add.assert_called_once()
|
|
script.presentMessage.assert_not_called()
|
|
|
|
callback, generation = idle_add.call_args.args
|
|
callback(generation)
|
|
script.presentMessage.assert_called_once_with(
|
|
messages.CLIPBOARD_COPIED_FULL,
|
|
messages.CLIPBOARD_COPIED_BRIEF,
|
|
)
|
|
|
|
script.updateKeyboardEventState(FakeKeyboardEvent(False, "c", CONTROL_MASK, source), None)
|
|
script.presentMessage.assert_called_once_with(
|
|
messages.CLIPBOARD_COPIED_FULL,
|
|
messages.CLIPBOARD_COPIED_BRIEF,
|
|
)
|
|
|
|
@mock.patch("cthulhu.scripts.default.AXUtilities.is_terminal", return_value=False)
|
|
@mock.patch("cthulhu.scripts.default.GLib.idle_add")
|
|
@mock.patch("cthulhu.scripts.default.GLib.timeout_add")
|
|
def test_wayland_keyboard_copy_handles_release_before_press_order_immediately(
|
|
self,
|
|
timeout_add,
|
|
idle_add,
|
|
_is_terminal,
|
|
):
|
|
script = self._make_script()
|
|
script._sessionType = "wayland"
|
|
source = object()
|
|
script.utilities.getClipboardContents.return_value = ""
|
|
script.utilities.allSelectedText.return_value = ("3086180", 0, 7)
|
|
script.utilities.selectedText.return_value = ("3086180", 0, 7)
|
|
script.utilities.objectContentsAreInClipboard.return_value = False
|
|
script.utilities.isInActiveApp.return_value = False
|
|
|
|
script.updateKeyboardEventState(
|
|
FakeKeyboardEvent(False, "0x3", CONTROL_MASK, source, "\x03"),
|
|
None,
|
|
)
|
|
script.updateKeyboardEventState(
|
|
FakeKeyboardEvent(True, "0x3", CONTROL_MASK, source, "\x03"),
|
|
None,
|
|
)
|
|
|
|
timeout_add.assert_not_called()
|
|
idle_add.assert_called_once()
|
|
script.presentMessage.assert_not_called()
|
|
|
|
callback, generation = idle_add.call_args.args
|
|
callback(generation)
|
|
script.presentMessage.assert_called_once_with(
|
|
messages.CLIPBOARD_COPIED_FULL,
|
|
messages.CLIPBOARD_COPIED_BRIEF,
|
|
)
|
|
|
|
@mock.patch("cthulhu.scripts.default.AXUtilities.is_terminal", return_value=False)
|
|
@mock.patch(
|
|
"cthulhu.scripts.default.GLib.timeout_add",
|
|
side_effect=lambda _delay, callback, generation: callback(generation),
|
|
)
|
|
def test_keyboard_copy_fallback_recognizes_control_character_key_name(
|
|
self,
|
|
_timeout_add,
|
|
_is_terminal,
|
|
):
|
|
script = self._make_script()
|
|
source = object()
|
|
script.utilities.getClipboardContents.side_effect = ["before", "copied text"]
|
|
script.utilities.objectContentsAreInClipboard.return_value = True
|
|
|
|
script.updateKeyboardEventState(
|
|
FakeKeyboardEvent(True, "0x3", CONTROL_MASK, source, "\x03"),
|
|
None,
|
|
)
|
|
script.updateKeyboardEventState(
|
|
FakeKeyboardEvent(False, "0x3", CONTROL_MASK, source, "\x03"),
|
|
None,
|
|
)
|
|
|
|
script.utilities.objectContentsAreInClipboard.assert_called_once_with(source)
|
|
script.presentMessage.assert_called_once_with(
|
|
messages.CLIPBOARD_COPIED_FULL,
|
|
messages.CLIPBOARD_COPIED_BRIEF,
|
|
)
|
|
|
|
@mock.patch("cthulhu.scripts.default.AXUtilities.is_terminal", return_value=False)
|
|
@mock.patch(
|
|
"cthulhu.scripts.default.GLib.timeout_add",
|
|
side_effect=lambda _delay, callback, generation: callback(generation),
|
|
)
|
|
def test_keyboard_copy_fallback_uses_captured_source_not_current_focus(
|
|
self,
|
|
_timeout_add,
|
|
_is_terminal,
|
|
):
|
|
script = self._make_script()
|
|
source = object()
|
|
script.utilities.getClipboardContents.side_effect = ["before", "copied text"]
|
|
script.utilities.objectContentsAreInClipboard.return_value = True
|
|
|
|
script.updateKeyboardEventState(FakeKeyboardEvent(True, "c", CONTROL_MASK, source), None)
|
|
script.updateKeyboardEventState(FakeKeyboardEvent(False, "c", CONTROL_MASK, object()), None)
|
|
|
|
script.utilities.objectContentsAreInClipboard.assert_called_once_with(source)
|
|
|
|
@mock.patch("cthulhu.scripts.default.AXUtilities.is_terminal", return_value=False)
|
|
@mock.patch(
|
|
"cthulhu.scripts.default.GLib.timeout_add",
|
|
side_effect=lambda _delay, callback, generation: callback(generation),
|
|
)
|
|
def test_keyboard_copy_fallback_stays_silent_if_clipboard_is_unchanged(
|
|
self,
|
|
_timeout_add,
|
|
_is_terminal,
|
|
):
|
|
script = self._make_script()
|
|
source = object()
|
|
script.utilities.getClipboardContents.side_effect = ["same text", "same text"]
|
|
script.utilities.objectContentsAreInClipboard.return_value = False
|
|
script.utilities.isInActiveApp.return_value = True
|
|
|
|
script.updateKeyboardEventState(FakeKeyboardEvent(True, "c", CONTROL_MASK, source), None)
|
|
script.updateKeyboardEventState(FakeKeyboardEvent(False, "c", CONTROL_MASK, source), None)
|
|
|
|
self.assertEqual(
|
|
script.utilities.objectContentsAreInClipboard.call_args_list,
|
|
[mock.call(source), mock.call()],
|
|
)
|
|
script.presentMessage.assert_not_called()
|
|
|
|
@mock.patch("cthulhu.scripts.default.AXUtilities.is_terminal", return_value=False)
|
|
@mock.patch(
|
|
"cthulhu.scripts.default.GLib.timeout_add",
|
|
side_effect=lambda _delay, callback, generation: callback(generation),
|
|
)
|
|
def test_keyboard_copy_fallback_presents_when_clipboard_unchanged_but_matches_source(
|
|
self,
|
|
_timeout_add,
|
|
_is_terminal,
|
|
):
|
|
script = self._make_script()
|
|
source = object()
|
|
script.utilities.getClipboardContents.side_effect = ["same text", "same text"]
|
|
script.utilities.objectContentsAreInClipboard.return_value = True
|
|
|
|
script.updateKeyboardEventState(FakeKeyboardEvent(True, "c", CONTROL_MASK, source), None)
|
|
script.updateKeyboardEventState(FakeKeyboardEvent(False, "c", CONTROL_MASK, source), None)
|
|
|
|
script.utilities.objectContentsAreInClipboard.assert_called_once_with(source)
|
|
script.presentMessage.assert_called_once_with(
|
|
messages.CLIPBOARD_COPIED_FULL,
|
|
messages.CLIPBOARD_COPIED_BRIEF,
|
|
)
|
|
|
|
@mock.patch("cthulhu.scripts.default.AXUtilities.is_terminal", return_value=False)
|
|
@mock.patch(
|
|
"cthulhu.scripts.default.GLib.timeout_add",
|
|
side_effect=lambda _delay, callback, generation: callback(generation),
|
|
)
|
|
def test_keyboard_copy_fallback_uses_current_focus_when_captured_source_no_longer_matches(
|
|
self,
|
|
_timeout_add,
|
|
_is_terminal,
|
|
):
|
|
script = self._make_script()
|
|
source = object()
|
|
script.utilities.getClipboardContents.side_effect = ["before", "copied text"]
|
|
script.utilities.objectContentsAreInClipboard.side_effect = [False, True]
|
|
script.utilities.isInActiveApp.return_value = True
|
|
|
|
script.updateKeyboardEventState(FakeKeyboardEvent(True, "c", CONTROL_MASK, source), None)
|
|
script.updateKeyboardEventState(FakeKeyboardEvent(False, "c", CONTROL_MASK, source), None)
|
|
|
|
self.assertEqual(
|
|
script.utilities.objectContentsAreInClipboard.call_args_list,
|
|
[mock.call(source), mock.call()],
|
|
)
|
|
script.presentMessage.assert_called_once_with(
|
|
messages.CLIPBOARD_COPIED_FULL,
|
|
messages.CLIPBOARD_COPIED_BRIEF,
|
|
)
|
|
|
|
@mock.patch("cthulhu.scripts.default.AXUtilities.is_terminal", return_value=False)
|
|
@mock.patch(
|
|
"cthulhu.scripts.default.GLib.timeout_add",
|
|
side_effect=lambda _delay, callback, generation: callback(generation),
|
|
)
|
|
def test_keyboard_copy_fallback_does_not_probe_current_focus_when_source_left_active_app(
|
|
self,
|
|
_timeout_add,
|
|
_is_terminal,
|
|
):
|
|
script = self._make_script()
|
|
source = object()
|
|
script.utilities.getClipboardContents.side_effect = ["before", "copied text"]
|
|
script.utilities.objectContentsAreInClipboard.return_value = False
|
|
script.utilities.isInActiveApp.return_value = False
|
|
|
|
script.updateKeyboardEventState(FakeKeyboardEvent(True, "c", CONTROL_MASK, source), None)
|
|
script.updateKeyboardEventState(FakeKeyboardEvent(False, "c", CONTROL_MASK, source), None)
|
|
|
|
script.utilities.objectContentsAreInClipboard.assert_called_once_with(source)
|
|
script.presentMessage.assert_not_called()
|
|
|
|
@mock.patch("cthulhu.scripts.default.AXUtilities.is_terminal", return_value=False)
|
|
@mock.patch(
|
|
"cthulhu.scripts.default.GLib.timeout_add",
|
|
side_effect=run_repeating_timeout,
|
|
)
|
|
def test_keyboard_copy_fallback_retries_until_clipboard_match_appears(
|
|
self,
|
|
_timeout_add,
|
|
_is_terminal,
|
|
):
|
|
script = self._make_script()
|
|
source = object()
|
|
script.utilities.getClipboardContents.side_effect = ["before", "before", "copied text"]
|
|
script.utilities.objectContentsAreInClipboard.side_effect = [False, True]
|
|
script.utilities.isInActiveApp.return_value = False
|
|
|
|
script.updateKeyboardEventState(FakeKeyboardEvent(True, "c", CONTROL_MASK, source), None)
|
|
script.updateKeyboardEventState(FakeKeyboardEvent(False, "c", CONTROL_MASK, source), None)
|
|
|
|
self.assertEqual(
|
|
script.utilities.objectContentsAreInClipboard.call_args_list,
|
|
[mock.call(source), mock.call(source)],
|
|
)
|
|
script.presentMessage.assert_called_once_with(
|
|
messages.CLIPBOARD_COPIED_FULL,
|
|
messages.CLIPBOARD_COPIED_BRIEF,
|
|
)
|
|
|
|
@mock.patch("cthulhu.scripts.default.AXUtilities.is_terminal", return_value=False)
|
|
@mock.patch(
|
|
"cthulhu.scripts.default.GLib.timeout_add",
|
|
side_effect=lambda _delay, callback, generation: callback(generation),
|
|
)
|
|
def test_keyboard_copy_fallback_uses_captured_selected_text_when_live_selection_clears(
|
|
self,
|
|
_timeout_add,
|
|
_is_terminal,
|
|
):
|
|
script = self._make_script()
|
|
source = object()
|
|
script.utilities.getClipboardContents.side_effect = ["before", "copied text"]
|
|
script.utilities.allSelectedText.return_value = ("copied text", 0, 11)
|
|
script.utilities.selectedText.return_value = ("", 0, 0)
|
|
script.utilities.objectContentsAreInClipboard.return_value = False
|
|
script.utilities.isInActiveApp.return_value = False
|
|
|
|
script.updateKeyboardEventState(FakeKeyboardEvent(True, "c", CONTROL_MASK, source), None)
|
|
script.updateKeyboardEventState(FakeKeyboardEvent(False, "c", CONTROL_MASK, source), None)
|
|
|
|
script.presentMessage.assert_called_once_with(
|
|
messages.CLIPBOARD_COPIED_FULL,
|
|
messages.CLIPBOARD_COPIED_BRIEF,
|
|
)
|
|
|
|
@mock.patch("cthulhu.scripts.default.AXUtilities.is_terminal", return_value=False)
|
|
@mock.patch(
|
|
"cthulhu.scripts.default.GLib.timeout_add",
|
|
side_effect=lambda _delay, callback, generation: callback(generation),
|
|
)
|
|
def test_clipboard_callback_after_fallback_does_not_duplicate_message(
|
|
self,
|
|
_timeout_add,
|
|
_is_terminal,
|
|
):
|
|
script = self._make_script()
|
|
source = object()
|
|
script.utilities.getClipboardContents.side_effect = ["before", "copied text", "copied text"]
|
|
script.utilities.objectContentsAreInClipboard.return_value = True
|
|
script.utilities.topLevelObjectIsActiveAndCurrent.return_value = True
|
|
script.utilities.lastInputEventWasCopy.return_value = True
|
|
|
|
script.updateKeyboardEventState(FakeKeyboardEvent(True, "c", CONTROL_MASK, source), None)
|
|
script.updateKeyboardEventState(FakeKeyboardEvent(False, "c", CONTROL_MASK, source), None)
|
|
script.onClipboardContentsChanged()
|
|
|
|
script.presentMessage.assert_called_once_with(
|
|
messages.CLIPBOARD_COPIED_FULL,
|
|
messages.CLIPBOARD_COPIED_BRIEF,
|
|
)
|
|
|
|
@mock.patch("cthulhu.scripts.default.AXUtilities.is_terminal", return_value=False)
|
|
@mock.patch(
|
|
"cthulhu.scripts.default.GLib.timeout_add",
|
|
side_effect=lambda _delay, callback, generation: callback(generation),
|
|
)
|
|
def test_clipboard_callback_without_match_does_not_cancel_keyboard_fallback(
|
|
self,
|
|
_timeout_add,
|
|
_is_terminal,
|
|
):
|
|
script = self._make_script()
|
|
source = object()
|
|
script.utilities.getClipboardContents.side_effect = ["before", "copied text", "copied text"]
|
|
script.utilities.objectContentsAreInClipboard.side_effect = [False, False, True]
|
|
script.utilities.isInActiveApp.return_value = False
|
|
|
|
script.updateKeyboardEventState(FakeKeyboardEvent(True, "c", CONTROL_MASK, source), None)
|
|
script.onClipboardContentsChanged()
|
|
script.updateKeyboardEventState(FakeKeyboardEvent(False, "c", CONTROL_MASK, source), None)
|
|
|
|
self.assertEqual(
|
|
script.utilities.objectContentsAreInClipboard.call_args_list,
|
|
[mock.call(source), mock.call(), mock.call(source)],
|
|
)
|
|
script.presentMessage.assert_called_once_with(
|
|
messages.CLIPBOARD_COPIED_FULL,
|
|
messages.CLIPBOARD_COPIED_BRIEF,
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|