Files
cthulhu/tests/test_web_input_regressions.py
2026-05-24 03:47:15 -04:00

564 lines
26 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()
class WebActiveWindowRegressionTests(unittest.TestCase):
def test_sanity_check_recovers_missing_script_app_from_active_window(self):
testScript = mock.Mock(app=None)
utilities = web_script_utilities.Utilities.__new__(web_script_utilities.Utilities)
utilities._script = testScript
activeWindow = object()
app = object()
with (
mock.patch.object(web_script_utilities.cthulhu_state, "activeWindow", activeWindow),
mock.patch.object(web_script_utilities.cthulhu_state, "locusOfFocus", None),
mock.patch.object(
web_script_utilities.AXObject,
"get_application",
side_effect=lambda obj: app if obj is activeWindow else None,
),
mock.patch.object(
web_script_utilities.AXObject,
"get_parent",
side_effect=lambda obj: app if obj is activeWindow else None,
),
mock.patch.object(web_script_utilities.cthulhu, "setActiveWindow") as setActiveWindow,
):
result = web_script_utilities.Utilities.sanityCheckActiveWindow(utilities)
self.assertTrue(result)
self.assertIs(testScript.app, app)
setActiveWindow.assert_not_called()
def test_sanity_check_preserves_current_window_when_recovery_is_inconclusive(self):
oldApp = object()
testScript = mock.Mock(app=oldApp)
utilities = web_script_utilities.Utilities.__new__(web_script_utilities.Utilities)
utilities._script = testScript
activeWindow = object()
with (
mock.patch.object(web_script_utilities.cthulhu_state, "activeWindow", activeWindow),
mock.patch.object(
web_script_utilities.AXObject,
"get_parent",
return_value=object(),
),
mock.patch.object(utilities, "activeWindow", return_value=None),
mock.patch.object(web_script_utilities.cthulhu, "setActiveWindow") as setActiveWindow,
):
result = web_script_utilities.Utilities.sanityCheckActiveWindow(utilities)
self.assertTrue(result)
self.assertIs(testScript.app, oldApp)
setActiveWindow.assert_not_called()
class WebPresentationModeSpeechRegressionTests(unittest.TestCase):
def _make_script(self, inFocusMode):
testScript = web_script.Script.__new__(web_script.Script)
testScript._inFocusMode = inFocusMode
testScript._focusModeIsSticky = True
testScript._browseModeIsSticky = True
testScript._loadingDocumentContent = False
testScript._lastCommandWasCaretNav = False
testScript._lastCommandWasStructNav = False
testScript.utilities = mock.Mock()
testScript.utilities.getCaretContext.return_value = ("button", 0)
testScript.utilities.grabFocusWhenSettingCaret.return_value = True
testScript.presentMessage = mock.Mock()
testScript.refreshKeyGrabs = mock.Mock()
testScript._shouldSuppressBrowseModeSound = mock.Mock(return_value=False)
return testScript
def test_automatic_browse_mode_announcement_does_not_interrupt_focus_presentation(self):
testScript = self._make_script(inFocusMode=True)
soundManager = mock.Mock()
with (
mock.patch.object(web_script.AXObject, "get_parent", return_value=None),
mock.patch.object(web_script.AXUtilities, "is_list_box", return_value=False),
mock.patch.object(web_script.AXUtilities, "is_menu", return_value=False),
mock.patch.object(web_script.sound_theme_manager, "getManager", return_value=soundManager),
):
web_script.Script.togglePresentationMode(testScript, None, "document")
testScript.presentMessage.assert_called_once_with(messages.MODE_BROWSE)
soundManager.playBrowseModeSound.assert_called_once_with()
self.assertFalse(testScript._inFocusMode)
self.assertFalse(testScript._focusModeIsSticky)
self.assertFalse(testScript._browseModeIsSticky)
testScript.refreshKeyGrabs.assert_called_once_with()
def test_automatic_focus_mode_announcement_does_not_interrupt_focus_presentation(self):
testScript = self._make_script(inFocusMode=False)
soundManager = mock.Mock()
with mock.patch.object(
web_script.sound_theme_manager,
"getManager",
return_value=soundManager,
):
web_script.Script.togglePresentationMode(testScript, None, "document")
testScript.presentMessage.assert_called_once_with(messages.MODE_FOCUS)
soundManager.playFocusModeSound.assert_called_once_with()
self.assertTrue(testScript._inFocusMode)
self.assertFalse(testScript._focusModeIsSticky)
self.assertFalse(testScript._browseModeIsSticky)
testScript.refreshKeyGrabs.assert_called_once_with()
def test_manual_presentation_mode_toggle_still_interrupts(self):
testScript = self._make_script(inFocusMode=False)
with mock.patch.object(web_script.sound_theme_manager, "getManager", return_value=mock.Mock()):
web_script.Script.togglePresentationMode(testScript, object(), "document")
testScript.presentMessage.assert_called_once_with(messages.MODE_FOCUS, interrupt=True)
class WebFocusSpeechInterruptionRegressionTests(unittest.TestCase):
def _make_script(self):
testScript = web_script.Script.__new__(web_script.Script)
testScript._navSuspended = False
testScript._lastCommandWasCaretNav = False
testScript._lastCommandWasStructNav = False
testScript._lastCommandWasMouseButton = False
testScript._inFocusMode = True
testScript._focusModeIsSticky = True
testScript._browseModeIsSticky = False
testScript.flatReviewPresenter = mock.Mock()
testScript.flatReviewPresenter.is_active.return_value = False
testScript.utilities = mock.Mock()
testScript.utilities.isZombie.return_value = False
testScript.utilities.isDocument.return_value = False
testScript.utilities.getTopLevelDocumentForObject.return_value = "document"
testScript.utilities.inFindContainer.return_value = False
testScript.utilities.queryNonEmptyText.return_value = None
testScript.utilities.isContentEditableWithEmbeddedObjects.return_value = False
testScript.utilities.isAnchor.return_value = False
testScript.utilities.lastInputEventWasPageNav.return_value = False
testScript.utilities.isFocusedWithMathChild.return_value = False
testScript.utilities.caretMovedToSamePageFragment.return_value = False
testScript.utilities.lastInputEventWasLineNav.return_value = False
testScript.utilities.inDocumentContent.return_value = True
testScript.utilities.shouldInterruptForLocusOfFocusChange.return_value = True
testScript.speechGenerator = mock.Mock()
testScript.speechGenerator.generateSpeech.return_value = ["focus speech"]
testScript.updateBraille = mock.Mock()
testScript.presentationInterrupt = mock.Mock()
testScript._saveFocusedObjectInfo = mock.Mock()
testScript.refreshKeyGrabs = mock.Mock()
return testScript
def test_focus_generated_speech_uses_explicit_interrupt_then_appends(self):
testScript = self._make_script()
oldFocus = object()
newFocus = object()
event = mock.Mock(type="object:state-changed:focused", source=newFocus)
with (
mock.patch.object(web_script.AXObject, "is_dead", return_value=False),
mock.patch.object(web_script.AXUtilities, "is_unknown_or_redundant", return_value=False),
mock.patch.object(web_script.AXUtilities, "is_heading", return_value=False),
mock.patch.object(web_script.speech, "speak") as speak,
mock.patch.object(web_script.cthulhu, "emitRegionChanged"),
):
result = web_script.Script.locus_of_focus_changed(
testScript, event, oldFocus, newFocus
)
self.assertTrue(result)
testScript.presentationInterrupt.assert_called_once_with()
speak.assert_called_once_with(["focus speech"], interrupt=False)
class WebDescriptionChangeRegressionTests(unittest.TestCase):
def _make_script(self):
testScript = web_script.Script.__new__(web_script.Script)
testScript.pointOfReference = {}
testScript.utilities = mock.Mock()
testScript.utilities.eventIsBrowserUINoise.return_value = False
testScript.utilities.inDocumentContent.return_value = True
testScript.utilities.stringsAreRedundant.side_effect = (
lambda first, second: first == second
)
testScript.presentMessage = mock.Mock()
return testScript
def test_document_description_change_matching_name_is_ignored(self):
testScript = self._make_script()
source = object()
event = mock.Mock(source=source, any_data="More")
with (
mock.patch.object(web_script.cthulhu_state, "locusOfFocus", source),
mock.patch.object(web_script.AXObject, "get_name", return_value="More"),
):
result = web_script.Script.onDescriptionChanged(testScript, event)
self.assertTrue(result)
testScript.presentMessage.assert_not_called()
def test_document_description_change_appends_to_focus_presentation(self):
testScript = self._make_script()
source = object()
event = mock.Mock(source=source, any_data="Helpful tooltip")
with (
mock.patch.object(web_script.cthulhu_state, "locusOfFocus", source),
mock.patch.object(web_script.AXObject, "get_name", return_value="Button"),
):
result = web_script.Script.onDescriptionChanged(testScript, event)
self.assertTrue(result)
testScript.presentMessage.assert_called_once_with("Helpful tooltip")
class WebDynamicContentRecoveryRegressionTests(unittest.TestCase):
def _make_dynamic_script(self):
testScript = web_script.Script.__new__(web_script.Script)
testScript._loadingDocumentContent = False
testScript._lastCommandWasCaretNav = False
testScript._lastCommandWasStructNav = False
testScript._lastMouseButtonContext = (None, -1)
testScript.lastMouseRoutingTime = 0
testScript.utilities = mock.Mock()
testScript.utilities.eventIsBrowserUINoise.return_value = False
testScript.utilities.isLiveRegion.return_value = False
testScript.utilities.getTopLevelDocumentForObject.return_value = "document"
testScript.utilities.isZombie.return_value = False
testScript.utilities.handleEventFromContextReplicant.return_value = False
testScript.utilities.handleEventForRemovedChild.return_value = False
testScript.utilities.inDocumentContent.return_value = True
return testScript
def test_children_added_to_live_focus_preserves_caret_context(self):
testScript = self._make_dynamic_script()
focus = object()
event = mock.Mock(source=focus, any_data=object())
with (
mock.patch.object(web_script.cthulhu_state, "locusOfFocus", focus),
mock.patch.object(web_script.AXObject, "is_dead", return_value=False),
mock.patch.object(web_script.AXUtilities, "is_busy", return_value=False),
mock.patch.object(web_script.AXUtilities, "is_alert", return_value=False),
):
result = web_script.Script.onChildrenAdded(testScript, event)
self.assertFalse(result)
testScript.utilities.dumpCache.assert_called_once_with(
"document",
preserveContext=True,
)
def test_children_removed_from_live_focus_preserves_caret_context(self):
testScript = self._make_dynamic_script()
focus = object()
event = mock.Mock(source=focus, any_data=object())
with (
mock.patch.object(web_script.cthulhu_state, "locusOfFocus", focus),
mock.patch.object(web_script.AXObject, "is_dead", return_value=False),
):
result = web_script.Script.onChildrenRemoved(testScript, event)
self.assertFalse(result)
testScript.utilities.dumpCache.assert_called_once_with(
"document",
preserveContext=True,
)
def test_focused_event_source_becomes_context_when_search_fails(self):
testScript = self._make_dynamic_script()
source = object()
oldFocus = object()
event = mock.Mock(detail1=1, source=source)
testScript._lastCommandWasCaretNav = True
testScript.utilities.getDocumentForObject.return_value = "document"
testScript.utilities.isWebAppDescendant.return_value = False
testScript.utilities.handleEventFromContextReplicant.return_value = False
testScript.utilities.getCaretContext.return_value = (None, -1)
testScript.utilities.searchForCaretContext.return_value = (None, -1)
testScript.utilities.inFindContainer.return_value = False
with (
mock.patch.object(web_script.cthulhu_state, "locusOfFocus", oldFocus),
mock.patch.object(web_script.AXUtilities, "is_editable", return_value=False),
mock.patch.object(web_script.AXUtilities, "is_dialog_or_alert", return_value=False),
mock.patch.object(web_script.AXUtilities, "is_focusable", return_value=True),
mock.patch.object(web_script.AXUtilities, "is_focused", return_value=True),
mock.patch.object(web_script.cthulhu, "setLocusOfFocus") as setLocusOfFocus,
):
result = web_script.Script.onFocusedChanged(testScript, event)
self.assertTrue(result)
setLocusOfFocus.assert_called_once_with(event, source, False)
testScript.utilities.setCaretContext.assert_called_once_with(source, 0)
class WebContextReplicantRegressionTests(unittest.TestCase):
def test_same_object_replicant_can_recover_before_old_focus_is_dead(self):
utilities = web_script_utilities.Utilities.__new__(web_script_utilities.Utilities)
document = object()
parent = object()
oldFocus = object()
replicant = object()
event = mock.Mock()
utilities._caretContexts = {hash(parent): (oldFocus, 0)}
utilities.getCaretContextPathRoleAndName = mock.Mock(
return_value=([1, 2, 3], "button", "storm, microphone on")
)
utilities.documentFrame = mock.Mock(return_value=document)
utilities.isSameObject = mock.Mock(return_value=True)
utilities.setCaretContext = mock.Mock()
with (
mock.patch.object(web_script_utilities.cthulhu_state, "locusOfFocus", oldFocus),
mock.patch.object(web_script_utilities.AXObject, "is_dead", return_value=False),
mock.patch.object(web_script_utilities.AXObject, "get_path", return_value=[1, 2, 3]),
mock.patch.object(web_script_utilities.AXObject, "get_role", return_value="button"),
mock.patch.object(web_script_utilities.AXObject, "get_name", return_value="storm, microphone on"),
mock.patch.object(web_script_utilities.AXObject, "get_parent", return_value=parent),
mock.patch.object(web_script_utilities.cthulhu, "setLocusOfFocus") as setLocusOfFocus,
):
result = web_script_utilities.Utilities.handleEventFromContextReplicant(
utilities,
event,
replicant,
)
self.assertTrue(result)
setLocusOfFocus.assert_called_once_with(event, replicant, False)
utilities.setCaretContext.assert_called_once_with(replicant, 0, document)
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)
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)
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])