Files
cthulhu/tests/test_default_script_clipboard_regressions.py
Hunter Jozwiak 04c79f2e0f Implement AT-SPI selection bridging groundwork
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.
2026-04-09 05:22:00 -04:00

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()