Files
cthulhu/tests/test_web_input_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

232 lines
10 KiB
Python

import os
import sys
import unittest
from pathlib import Path
from unittest import mock
import gi
os.environ.setdefault("GSETTINGS_BACKEND", "memory")
gi.require_version("Gdk", "3.0")
gi.require_version("Gtk", "3.0")
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
soundGeneratorModule = sys.modules.get("cthulhu.sound_generator")
if soundGeneratorModule is not None and not hasattr(soundGeneratorModule, "SoundGenerator"):
class _StubSoundGenerator:
pass
soundGeneratorModule.SoundGenerator = _StubSoundGenerator
from cthulhu import messages
from cthulhu.scripts.web import script as web_script
from cthulhu.scripts.web import script_utilities as web_script_utilities
class WebKeyGrabRegressionTests(unittest.TestCase):
def _make_partial_script(self):
testScript = web_script.Script.__new__(web_script.Script)
testScript._lastCommandWasCaretNav = True
testScript._lastCommandWasStructNav = True
testScript._lastCommandWasMouseButton = True
testScript._lastMouseButtonContext = ("old", 7)
testScript._madeFindAnnouncement = True
testScript._inFocusMode = True
testScript._navSuspended = False
testScript.removeKeyGrabs = mock.Mock()
testScript.refreshKeyGrabs = mock.Mock()
testScript._setNavigationSuspended = mock.Mock()
testScript.utilities = mock.Mock()
testScript.utilities.isZombie.return_value = False
testScript.utilities.isDocument.return_value = False
testScript.utilities.getTopLevelDocumentForObject.return_value = None
return testScript
def test_window_deactivate_does_not_drop_key_grabs(self):
testScript = self._make_partial_script()
result = web_script.Script.onWindowDeactivated(testScript, mock.Mock())
self.assertFalse(result)
self.assertFalse(testScript._lastCommandWasCaretNav)
self.assertFalse(testScript._lastCommandWasStructNav)
self.assertFalse(testScript._lastCommandWasMouseButton)
self.assertEqual(testScript._lastMouseButtonContext, (None, -1))
testScript.removeKeyGrabs.assert_not_called()
def test_non_document_focus_change_without_prior_document_does_not_refresh_grabs(self):
testScript = self._make_partial_script()
oldFocus = object()
newFocus = object()
with mock.patch("cthulhu.scripts.web.script.AXObject.is_dead", return_value=False):
result = web_script.Script.locus_of_focus_changed(testScript, None, oldFocus, newFocus)
self.assertFalse(result)
self.assertFalse(testScript._madeFindAnnouncement)
self.assertFalse(testScript._inFocusMode)
testScript._setNavigationSuspended.assert_called_once_with(
True,
"focus left document content",
)
testScript.refreshKeyGrabs.assert_not_called()
def test_non_document_focus_change_refreshes_grabs_after_leaving_document(self):
testScript = self._make_partial_script()
oldFocus = object()
newFocus = object()
oldDocument = object()
testScript.utilities.getTopLevelDocumentForObject.side_effect = (
lambda obj: oldDocument if obj is oldFocus else None
)
with mock.patch("cthulhu.scripts.web.script.AXObject.is_dead", return_value=False):
result = web_script.Script.locus_of_focus_changed(testScript, None, oldFocus, newFocus)
self.assertFalse(result)
testScript.refreshKeyGrabs.assert_called_once_with()
def test_non_document_focus_change_refreshes_grabs_when_old_focus_missing(self):
testScript = self._make_partial_script()
newFocus = object()
with mock.patch("cthulhu.scripts.web.script.AXObject.is_dead", return_value=False):
result = web_script.Script.locus_of_focus_changed(testScript, None, None, newFocus)
self.assertFalse(result)
testScript.refreshKeyGrabs.assert_called_once_with()
if __name__ == "__main__":
unittest.main()
class WebSelectionRegressionTests(unittest.TestCase):
def _make_script(self):
testScript = web_script.Script.__new__(web_script.Script)
testScript.pointOfReference = {}
testScript.utilities = mock.Mock()
testScript.utilities.sanityCheckActiveWindow = mock.Mock()
testScript.utilities.isZombie.return_value = False
testScript.utilities.eventIsBrowserUINoise.return_value = False
testScript.utilities.eventIsBrowserUIAutocompleteNoise.return_value = False
testScript.utilities.lastInputEventWasTab.return_value = False
testScript.utilities.inFindContainer.return_value = False
testScript.utilities.eventIsFromLocusOfFocusDocument.return_value = True
testScript.utilities.textEventIsDueToInsertion.return_value = False
testScript.utilities.textEventIsDueToDeletion.return_value = False
testScript.utilities.isItemForEditableComboBox.return_value = False
testScript.utilities.eventIsAutocompleteNoise.return_value = False
testScript.utilities.eventIsSpinnerNoise.return_value = False
testScript.utilities.caretMovedOutsideActiveGrid.return_value = False
testScript.utilities.treatEventAsSpinnerValueChange.return_value = False
testScript.utilities.queryNonEmptyText.return_value = "Darkest Dungeon"
testScript.utilities.lastInputEventWasCaretNavWithSelection.return_value = True
testScript.utilities.getCharacterContentsAtOffset.return_value = [[None, 0, 1, "D"]]
testScript.utilities.pathComparison.return_value = -1
testScript.updateBraille = mock.Mock()
testScript.speakContents = mock.Mock()
testScript.speakMessage = mock.Mock()
testScript._lastCommandWasCaretNav = True
testScript._lastCommandWasStructNav = False
testScript._lastCommandWasMouseButton = False
testScript._lastMouseButtonContext = (None, -1)
testScript._inFocusMode = False
testScript._focusModeIsSticky = False
testScript._browseModeIsSticky = False
return testScript
def test_broken_web_caret_selection_synthesizes_selection_from_caret_context(self):
testScript = self._make_script()
document = object()
section = object()
event = mock.Mock(source=section, detail1=-1)
manager = mock.Mock()
manager.last_event_was_forward_caret_selection.return_value = True
manager.last_event_was_backward_caret_selection.return_value = False
testScript.utilities.getTopLevelDocumentForObject.return_value = document
testScript.utilities.getCaretContext.return_value = (section, 0)
testScript.utilities.nextContext.return_value = (section, 1)
testScript.utilities.getCharacterContentsAtOffset.return_value = [[section, 0, 1, "D"]]
with (
mock.patch.object(web_script.cthulhu_state, "locusOfFocus", section),
mock.patch.object(web_script.input_event_manager, "get_manager", return_value=manager),
mock.patch.object(web_script.cthulhu, "setLocusOfFocus") as setLocusOfFocus,
):
result = web_script.Script.onCaretMoved(testScript, event)
self.assertTrue(result)
testScript.utilities.setCaretContext.assert_called_once_with(section, 1, document)
setLocusOfFocus.assert_called_once_with(event, section, False, True)
testScript.speakContents.assert_called_once_with([[section, 0, 1, "D"]])
testScript.speakMessage.assert_called_once_with(messages.TEXT_SELECTED, interrupt=False)
self.assertEqual(
testScript.pointOfReference["syntheticWebSelection"]["string"],
"D",
)
def test_broken_web_text_selection_event_synthesizes_selection_from_event_source(self):
testScript = self._make_script()
document = object()
anchor = object()
link = object()
event = mock.Mock(source=link)
testScript.utilities.getTopLevelDocumentForObject.return_value = document
testScript.utilities.getCaretContext.return_value = (anchor, 0)
testScript.utilities.nextContext.side_effect = [(anchor, 0)]
testScript.utilities.getCharacterContentsAtOffset.side_effect = (
lambda obj, _offset: [[link, 0, 1, "S"]] if obj is link else []
)
testScript.utilities.textEventIsForNonNavigableTextObject.return_value = False
testScript.utilities.isContentEditableWithEmbeddedObjects.return_value = False
testScript._compareCaretContexts = mock.Mock(return_value=1)
with (
mock.patch.object(web_script.cthulhu_state, "locusOfFocus", anchor),
mock.patch.object(web_script.AXText, "get_caret_offset", return_value=0),
mock.patch.object(web_script.AXText, "get_selected_ranges", return_value=[]),
mock.patch.object(web_script.AXText, "get_substring", return_value="S"),
mock.patch.object(web_script.cthulhu, "setLocusOfFocus") as setLocusOfFocus,
):
result = web_script.Script.onTextSelectionChanged(testScript, event)
self.assertTrue(result)
testScript.utilities.setCaretContext.assert_called_once_with(link, 0, document)
setLocusOfFocus.assert_called_once_with(event, link, False, True)
testScript.speakContents.assert_called_once_with([[link, 0, 1, "S"]])
testScript.speakMessage.assert_called_once_with(messages.TEXT_SELECTED, interrupt=False)
self.assertEqual(
testScript.pointOfReference["syntheticWebSelection"]["string"],
"S",
)
def test_web_utilities_all_selected_text_falls_back_to_synthetic_selection(self):
testScript = mock.Mock(pointOfReference={})
utilities = web_script_utilities.Utilities.__new__(web_script_utilities.Utilities)
utilities._script = testScript
document = object()
obj = object()
testScript.pointOfReference["syntheticWebSelection"] = {
"document": document,
"string": "Dark",
}
with (
mock.patch.object(
web_script_utilities.script_utilities.Utilities,
"allSelectedText",
return_value=["", 0, 0],
),
mock.patch.object(utilities, "inDocumentContent", return_value=True),
mock.patch.object(utilities, "getTopLevelDocumentForObject", return_value=document),
):
result = web_script_utilities.Utilities.allSelectedText(utilities, obj)
self.assertEqual(result, ["Dark", 0, 4])