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.
This commit is contained in:
@@ -20,7 +20,9 @@ if soundGeneratorModule is not None and not hasattr(soundGeneratorModule, "Sound
|
||||
|
||||
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):
|
||||
@@ -100,3 +102,130 @@ class WebKeyGrabRegressionTests(unittest.TestCase):
|
||||
|
||||
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])
|
||||
|
||||
Reference in New Issue
Block a user