From e3427af1add219b8e002206bde0b24d7d629ffd4 Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Fri, 24 Jul 2026 19:45:47 -0400 Subject: [PATCH] Updated tests. --- tests/test_web_event_sequences.py | 218 +++++++++++++++++++++++++++++- 1 file changed, 217 insertions(+), 1 deletion(-) diff --git a/tests/test_web_event_sequences.py b/tests/test_web_event_sequences.py index 3b49b58..0123b0d 100644 --- a/tests/test_web_event_sequences.py +++ b/tests/test_web_event_sequences.py @@ -11,7 +11,7 @@ gi.require_version("Atspi", "2.0") from gi.repository import Atspi from cthulhu import cthulhu_state -from cthulhu.ax_utilities_event import AXUtilitiesEvent +from cthulhu.ax_utilities_event import AXUtilitiesEvent, TextEventReason from cthulhu.scripts import default from cthulhu.scripts.toolkits.Chromium import script as chromium_script from cthulhu.scripts.toolkits.Gecko import script as gecko_script @@ -187,6 +187,222 @@ class ChromiumTabFocusSequenceTests(unittest.TestCase): self._assert_tab_caret_focused_sequence_presents_once(radio) +class FocusedChangeInteractionSequenceTests(unittest.TestCase): + """Ordered ownership contracts around focused-change events.""" + + @staticmethod + def _make_script( + document: object, + caretContext: FakeAccessible, + ) -> chromium_script.Script: + testScript = chromium_script.Script.__new__(chromium_script.Script) + testScript._lastCommandWasCaretNav = False + testScript._lastCommandWasStructNav = False + testScript._lastCommandWasMouseButton = False + testScript._browseModeIsSticky = False + testScript._clearSyntheticWebSelection = mock.Mock() + testScript.refreshKeyGrabs = mock.Mock() + testScript.utilities = mock.Mock() + testScript.utilities.isDocument.return_value = False + testScript.utilities.isStaticTextLeaf.return_value = False + testScript.utilities.isRedundantAutocompleteEvent.return_value = False + testScript.utilities.isZombie.return_value = False + testScript.utilities.getTopLevelDocumentForObject.return_value = document + testScript.utilities.getDocumentForObject.return_value = document + testScript.utilities.getCaretContext.return_value = (caretContext, 0) + testScript.utilities.lastInputEventWasCaretNavWithSelection.return_value = False + testScript.utilities.lastInputEventWasCharNav.return_value = False + testScript.utilities.inFindContainer.return_value = False + testScript.utilities.eventIsFromLocusOfFocusDocument.return_value = True + testScript.utilities.isWebAppDescendant.return_value = False + testScript.utilities.handleEventFromContextReplicant.return_value = False + testScript.utilities.lastInputEventWasPageNav.return_value = False + testScript.utilities.isAnchor.return_value = False + testScript.utilities.isLink.return_value = False + testScript.utilities.isChildOfCurrentFragment.return_value = False + testScript.utilities.documentFragment.return_value = "" + return testScript + + def _assert_navigation_command_owns_caret_and_focus_pair( + self, + *, + caretNavigation: bool, + structuralNavigation: bool, + ) -> None: + document = object() + target = FakeAccessible(Atspi.Role.PARAGRAPH) + testScript = self._make_script(document, target) + testScript._lastCommandWasCaretNav = caretNavigation + testScript._lastCommandWasStructNav = structuralNavigation + events = [ + FakeEvent("object:text-caret-moved", target, detail1=4), + FakeEvent("object:state-changed:focused", target, detail1=1), + ] + presentations = ["navigation-command"] + + with ( + mock.patch.object(cthulhu_state, "locusOfFocus", target), + mock.patch.object( + AXUtilitiesEvent, + "get_text_event_reason", + return_value=TextEventReason.NAVIGATION_BY_CHARACTER, + ), + 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.AXUtilities, "is_document", return_value=False), + mock.patch.object( + default.Script, + "onCaretMoved", + side_effect=lambda _script, _event: presentations.append("caret-event"), + ) as defaultCaretHandler, + mock.patch.object( + default.Script, + "onFocusedChanged", + side_effect=lambda _script, _event: presentations.append("focus-event"), + ) as defaultFocusHandler, + ): + WebEventSequence(testScript).run(events) + + self.assertEqual(presentations, ["navigation-command"]) + defaultCaretHandler.assert_not_called() + defaultFocusHandler.assert_not_called() + + def test_caret_navigation_owns_following_caret_and_focus_events(self) -> None: + self._assert_navigation_command_owns_caret_and_focus_pair( + caretNavigation=True, + structuralNavigation=False, + ) + + def test_structural_navigation_owns_following_caret_and_focus_events(self) -> None: + self._assert_navigation_command_owns_caret_and_focus_pair( + caretNavigation=False, + structuralNavigation=True, + ) + + def test_tab_clears_navigation_ownership_and_real_focus_still_presents(self) -> None: + document = object() + oldTarget = FakeAccessible(Atspi.Role.PARAGRAPH) + newTarget = FakeAccessible(Atspi.Role.ENTRY, editable=True) + testScript = self._make_script(document, oldTarget) + testScript._lastCommandWasCaretNav = True + ignoredFocus = FakeEvent( + "object:state-changed:focused", + oldTarget, + detail1=1, + ) + realFocus = FakeEvent( + "object:state-changed:focused", + newTarget, + detail1=1, + ) + tabEvent = mock.Mock(event_string="Tab") + tabEvent.is_modifier_key.return_value = False + presentations: list[FakeAccessible] = [] + + with ( + mock.patch.object(cthulhu_state, "locusOfFocus", oldTarget), + mock.patch.object( + web_script.AXUtilities, + "is_editable", + side_effect=lambda obj: obj.editable, + ), + 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.AXUtilities, "is_document", return_value=False), + mock.patch.object(default.Script, "shouldConsumeKeyboardEvent", return_value=False), + mock.patch.object( + default.Script, + "onFocusedChanged", + side_effect=lambda _script, event: presentations.append(event.source), + ) as defaultFocusHandler, + ): + WebEventSequence(testScript).run([ignoredFocus]) + web_script.Script.shouldConsumeKeyboardEvent(testScript, tabEvent, None) + WebEventSequence(testScript).run([realFocus]) + + self.assertEqual(presentations, [newTarget]) + self.assertFalse(testScript._lastCommandWasCaretNav) + self.assertFalse(testScript._lastCommandWasStructNav) + defaultFocusHandler.assert_called_once_with(testScript, realFocus) + + def _assert_document_focus_uses_recovered_context_once( + self, + *, + pageNavigation: bool, + documentFragment: str, + ) -> None: + document = FakeAccessible(Atspi.Role.DOCUMENT_WEB) + oldFocus = FakeAccessible(Atspi.Role.FRAME) + context = FakeAccessible(Atspi.Role.LINK) + testScript = self._make_script(document, context) + testScript.utilities.getDocumentForObject.side_effect = ( + lambda obj: document if obj is document else None + ) + testScript.utilities.getCaretContext.return_value = (None, -1) + testScript.utilities.searchForCaretContext.return_value = (context, 6) + testScript.utilities.lastInputEventWasPageNav.return_value = pageNavigation + testScript.utilities.isLink.side_effect = lambda obj: obj is context + testScript.utilities.documentFragment.return_value = documentFragment + event = FakeEvent("object:state-changed:focused", document, detail1=1) + presentations: list[FakeAccessible] = [] + + def set_focus( + _event: FakeEvent, + obj: FakeAccessible, + notifyScript: bool = True, + ) -> None: + if notifyScript: + presentations.append(obj) + + with ( + mock.patch.object(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", + side_effect=lambda obj: obj is document, + ), + mock.patch.object( + web_script.AXUtilities, + "is_document", + side_effect=lambda obj: obj is document, + ), + mock.patch.object(web_script.AXObject, "clear_cache"), + mock.patch.object(web_script.cthulhu, "setLocusOfFocus", side_effect=set_focus) as setFocus, + mock.patch.object(default.Script, "onFocusedChanged") as defaultFocusHandler, + ): + WebEventSequence(testScript).run([event]) + + self.assertEqual(presentations, [context]) + self.assertEqual( + setFocus.call_args_list, + [mock.call(event, context, False), mock.call(event, context)], + ) + testScript.utilities.setCaretContext.assert_has_calls( + [mock.call(context, 6), mock.call(context, 6)] + if pageNavigation + else [mock.call(context, 6)] + ) + defaultFocusHandler.assert_not_called() + + def test_page_navigation_focus_uses_recovered_caret_context_once(self) -> None: + self._assert_document_focus_uses_recovered_context_once( + pageNavigation=True, + documentFragment="", + ) + + def test_page_fragment_focus_uses_recovered_caret_context_once(self) -> None: + self._assert_document_focus_uses_recovered_context_once( + pageNavigation=False, + documentFragment="#details", + ) + + class ChromiumNativeContainerSequenceTests(unittest.TestCase): """Current routing contracts for native containers in Chromium content."""