From edc195c46b6d6733df317aa951704941ee57e054 Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Sun, 24 May 2026 03:47:15 -0400 Subject: [PATCH] Some web updates. --- src/cthulhu/scripts/web/script.py | 18 ++- src/cthulhu/scripts/web/script_utilities.py | 3 +- tests/test_web_input_regressions.py | 118 ++++++++++++++++++++ 3 files changed, 136 insertions(+), 3 deletions(-) diff --git a/src/cthulhu/scripts/web/script.py b/src/cthulhu/scripts/web/script.py index 1dde9f4..5232aff 100644 --- a/src/cthulhu/scripts/web/script.py +++ b/src/cthulhu/scripts/web/script.py @@ -2276,7 +2276,10 @@ class Script(default.Script): tokens = ["WEB: Dumping cache and context: source is focus", cthulhu_state.locusOfFocus] debug.printTokens(debug.LEVEL_INFO, tokens, True) - self.utilities.dumpCache(document, preserveContext=False) + self.utilities.dumpCache( + document, + preserveContext=not AXObject.is_dead(event.source), + ) elif AXObject.is_dead(cthulhu_state.locusOfFocus): msg = "WEB: Dumping cache: dead focus" debug.printMessage(debug.LEVEL_INFO, msg, True) @@ -2389,7 +2392,10 @@ class Script(default.Script): tokens = ["WEB: Dumping cache and context: source is focus", cthulhu_state.locusOfFocus] debug.printTokens(debug.LEVEL_INFO, tokens, True) - self.utilities.dumpCache(document, preserveContext=False) + self.utilities.dumpCache( + document, + preserveContext=not AXObject.is_dead(event.source), + ) elif AXObject.is_dead(cthulhu_state.locusOfFocus): msg = "WEB: Dumping cache: dead focus" debug.printMessage(debug.LEVEL_INFO, msg, True) @@ -2594,6 +2600,14 @@ class Script(default.Script): else: msg = "WEB: Search for caret context failed" debug.printMessage(debug.LEVEL_INFO, msg, True) + if AXUtilities.is_focusable(event.source) \ + and AXUtilities.is_focused(event.source) \ + and self.utilities.inDocumentContent(event.source): + msg = "WEB: Falling back to focused event source as caret context" + debug.printMessage(debug.LEVEL_INFO, msg, True) + cthulhu.setLocusOfFocus(event, event.source, False) + self.utilities.setCaretContext(event.source, 0) + obj, offset = event.source, 0 if self._lastCommandWasCaretNav: msg = "WEB: Event ignored: Last command was caret nav" diff --git a/src/cthulhu/scripts/web/script_utilities.py b/src/cthulhu/scripts/web/script_utilities.py index f9949df..05012b6 100644 --- a/src/cthulhu/scripts/web/script_utilities.py +++ b/src/cthulhu/scripts/web/script_utilities.py @@ -5050,7 +5050,8 @@ class Utilities(script_utilities.Utilities): debug.printMessage(debug.LEVEL_INFO, msg, True) return False - if not AXObject.is_dead(cthulhu_state.locusOfFocus): + if not AXObject.is_dead(cthulhu_state.locusOfFocus) \ + and not self.isSameObject(replicant, cthulhu_state.locusOfFocus, True, True): tokens = ["WEB: Not event from context replicant. locusOfFocus", cthulhu_state.locusOfFocus, "is not dead."] debug.printTokens(debug.LEVEL_INFO, tokens, True) diff --git a/tests/test_web_input_regressions.py b/tests/test_web_input_regressions.py index 00bbf84..9fa762b 100644 --- a/tests/test_web_input_regressions.py +++ b/tests/test_web_input_regressions.py @@ -314,6 +314,124 @@ class WebDescriptionChangeRegressionTests(unittest.TestCase): 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()