1217 lines
52 KiB
Python
1217 lines
52 KiB
Python
"""Ordered browser event-sequence regression tests."""
|
|
|
|
import unittest
|
|
from dataclasses import dataclass
|
|
from typing import Any
|
|
from unittest import mock
|
|
|
|
import gi
|
|
|
|
gi.require_version("Atspi", "2.0")
|
|
from gi.repository import Atspi
|
|
|
|
from cthulhu import cthulhu_state
|
|
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
|
|
from cthulhu.scripts.web import script as web_script
|
|
|
|
|
|
@dataclass(eq=False)
|
|
class FakeAccessible:
|
|
"""Minimal accessible metadata needed to describe a sequence target."""
|
|
|
|
role: Atspi.Role
|
|
editable: bool = False
|
|
|
|
|
|
@dataclass(eq=False)
|
|
class FakeEvent:
|
|
"""Minimal AT-SPI event used by ordered web regression fixtures."""
|
|
|
|
type: str
|
|
source: FakeAccessible
|
|
detail1: int = 0
|
|
any_data: FakeAccessible | None = None
|
|
|
|
|
|
class WebEventSequence:
|
|
"""Dispatch an ordered event list through Chromium's current routing path."""
|
|
|
|
def __init__(self, testScript: chromium_script.Script) -> None:
|
|
self.testScript = testScript
|
|
|
|
def run(self, events: list[FakeEvent]) -> None:
|
|
handlers = {
|
|
"object:active-descendant-changed": chromium_script.Script.onActiveDescendantChanged,
|
|
"object:children-changed:add": chromium_script.Script.onChildrenAdded,
|
|
"object:children-changed:remove": chromium_script.Script.onChildrenRemoved,
|
|
"object:selection-changed": chromium_script.Script.onSelectionChanged,
|
|
"object:state-changed:busy": chromium_script.Script.onBusyChanged,
|
|
"object:text-caret-moved": chromium_script.Script.onCaretMoved,
|
|
"object:text-selection-changed": chromium_script.Script.onTextSelectionChanged,
|
|
"object:state-changed:focused": chromium_script.Script.onFocusedChanged,
|
|
"document:load-complete": chromium_script.Script.onDocumentLoadComplete,
|
|
"document:reload": chromium_script.Script.onDocumentReload,
|
|
}
|
|
for event in events:
|
|
handler = handlers[event.type]
|
|
handler(self.testScript, event)
|
|
|
|
|
|
class GeckoEventSequence:
|
|
"""Dispatch an ordered event list through Gecko's current routing path."""
|
|
|
|
def __init__(self, testScript: gecko_script.Script) -> None:
|
|
self.testScript = testScript
|
|
|
|
def run(self, events: list[FakeEvent]) -> None:
|
|
handlers = {
|
|
"object:text-caret-moved": gecko_script.Script.onCaretMoved,
|
|
"object:state-changed:focused": gecko_script.Script.onFocusedChanged,
|
|
}
|
|
for event in events:
|
|
handler = handlers[event.type]
|
|
handler(self.testScript, event)
|
|
|
|
|
|
class ChromiumTabFocusSequenceTests(unittest.TestCase):
|
|
"""Behavior contracts for controls reached by Tab in Chromium."""
|
|
|
|
@staticmethod
|
|
def _make_script(document: object, oldFocus: object) -> chromium_script.Script:
|
|
testScript = chromium_script.Script.__new__(chromium_script.Script)
|
|
testScript._lastCommandWasCaretNav = False
|
|
testScript._lastCommandWasStructNav = False
|
|
testScript._lastCommandWasMouseButton = False
|
|
testScript._clearSyntheticWebSelection = mock.Mock()
|
|
testScript.utilities = mock.Mock()
|
|
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.getCaretContext.return_value = (oldFocus, 0)
|
|
testScript.utilities.lastInputEventWasCaretNavWithSelection.return_value = False
|
|
testScript.utilities.lastInputEventWasCharNav.return_value = False
|
|
testScript.utilities.getDocumentForObject.return_value = document
|
|
testScript.utilities.isWebAppDescendant.return_value = False
|
|
testScript.utilities.handleEventFromContextReplicant.return_value = False
|
|
return testScript
|
|
|
|
def _assert_tab_caret_focused_sequence_presents_once(
|
|
self,
|
|
target: FakeAccessible,
|
|
) -> None:
|
|
document = object()
|
|
oldFocus = object()
|
|
testScript = self._make_script(document, oldFocus)
|
|
inputManager = mock.Mock()
|
|
inputManager.last_event_was_caret_selection.return_value = False
|
|
inputManager.last_event_was_caret_navigation.return_value = False
|
|
inputManager.last_event_was_select_all.return_value = False
|
|
inputManager.last_event_was_primary_click_or_release.return_value = False
|
|
inputManager.last_event_was_tab_navigation.return_value = True
|
|
presentations: list[Any] = []
|
|
events = [
|
|
FakeEvent("object:text-caret-moved", target, detail1=0),
|
|
FakeEvent("object:state-changed:focused", target, detail1=1),
|
|
]
|
|
|
|
with (
|
|
mock.patch.object(cthulhu_state, "locusOfFocus", oldFocus),
|
|
mock.patch(
|
|
"cthulhu.input_event_manager.get_manager",
|
|
return_value=inputManager,
|
|
),
|
|
mock.patch(
|
|
"cthulhu.ax_utilities_event.focus_manager.get_manager"
|
|
) as getFocusManager,
|
|
mock.patch(
|
|
"cthulhu.ax_utilities_event.AXUtilitiesRole.is_text_input_search",
|
|
return_value=False,
|
|
),
|
|
mock.patch(
|
|
"cthulhu.ax_utilities_event.AXUtilitiesState.is_editable",
|
|
side_effect=lambda obj: obj.editable,
|
|
),
|
|
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,
|
|
"onCaretMoved",
|
|
side_effect=lambda _script, event: presentations.append(event.source),
|
|
) as defaultCaretHandler,
|
|
mock.patch.object(
|
|
default.Script,
|
|
"onFocusedChanged",
|
|
side_effect=lambda _script, event: presentations.append(event.source),
|
|
) as defaultFocusHandler,
|
|
):
|
|
getFocusManager.return_value.get_active_mode_and_object_of_interest.return_value = (
|
|
None,
|
|
oldFocus,
|
|
)
|
|
WebEventSequence(testScript).run(events)
|
|
|
|
self.assertEqual(presentations, [target])
|
|
defaultCaretHandler.assert_not_called()
|
|
defaultFocusHandler.assert_called_once_with(testScript, events[1])
|
|
testScript._clearSyntheticWebSelection.assert_called_once_with()
|
|
|
|
def test_tab_caret_focused_sequence_presents_editable_custom_button_once(self) -> None:
|
|
customButton = FakeAccessible(Atspi.Role.PUSH_BUTTON, editable=True)
|
|
|
|
self._assert_tab_caret_focused_sequence_presents_once(customButton)
|
|
|
|
def test_tab_caret_focused_sequence_presents_native_entry_once(self) -> None:
|
|
entry = FakeAccessible(Atspi.Role.ENTRY, editable=True)
|
|
|
|
self._assert_tab_caret_focused_sequence_presents_once(entry)
|
|
|
|
def test_tab_caret_focused_sequence_presents_native_checkbox_once(self) -> None:
|
|
checkbox = FakeAccessible(Atspi.Role.CHECK_BOX)
|
|
|
|
self._assert_tab_caret_focused_sequence_presents_once(checkbox)
|
|
|
|
def test_tab_caret_focused_sequence_presents_native_radio_once(self) -> None:
|
|
radio = FakeAccessible(Atspi.Role.RADIO_BUTTON)
|
|
|
|
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."""
|
|
|
|
@staticmethod
|
|
def _make_script(document: object, caretContext: object) -> chromium_script.Script:
|
|
testScript = chromium_script.Script.__new__(chromium_script.Script)
|
|
testScript._lastCommandWasCaretNav = False
|
|
testScript._lastCommandWasStructNav = False
|
|
testScript._browseModeIsSticky = False
|
|
testScript.utilities = mock.Mock()
|
|
testScript.utilities.isDocument.return_value = False
|
|
testScript.utilities.isZombie.return_value = False
|
|
testScript.utilities.getDocumentForObject.return_value = document
|
|
testScript.utilities.isWebAppDescendant.return_value = False
|
|
testScript.utilities.handleEventFromContextReplicant.return_value = False
|
|
testScript.utilities.getCaretContext.return_value = (caretContext, 0)
|
|
testScript.utilities.inDocumentContent.return_value = True
|
|
testScript.utilities.eventIsBrowserUIAutocompleteNoise.return_value = False
|
|
testScript.utilities.eventIsBrowserUIPageSwitch.return_value = False
|
|
testScript.utilities.eventIsFromLocusOfFocusDocument.return_value = True
|
|
testScript.utilities.eventIsIrrelevantSelectionChangedEvent.return_value = False
|
|
testScript.utilities.commonAncestor.return_value = None
|
|
return testScript
|
|
|
|
def _run_container_sequence(
|
|
self,
|
|
events: list[FakeEvent],
|
|
expectedOwner: str,
|
|
) -> None:
|
|
document = object()
|
|
oldFocus = object()
|
|
testScript = self._make_script(document, oldFocus)
|
|
presentations: list[tuple[str, FakeAccessible]] = []
|
|
|
|
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", return_value=True),
|
|
mock.patch.object(web_script.AXUtilities, "is_document", return_value=False),
|
|
mock.patch.object(
|
|
default.Script,
|
|
"onActiveDescendantChanged",
|
|
side_effect=lambda _script, event: presentations.append(
|
|
("active-descendant", event.any_data)
|
|
),
|
|
) as defaultActiveDescendantHandler,
|
|
mock.patch.object(
|
|
default.Script,
|
|
"onSelectionChanged",
|
|
side_effect=lambda _script, event: presentations.append(
|
|
("selection", event.source)
|
|
),
|
|
) as defaultSelectionHandler,
|
|
mock.patch.object(
|
|
default.Script,
|
|
"onFocusedChanged",
|
|
side_effect=lambda _script, event: presentations.append(("focus", event.source)),
|
|
) as defaultFocusHandler,
|
|
):
|
|
WebEventSequence(testScript).run(events)
|
|
|
|
self.assertEqual(presentations, [(expectedOwner, events[-1].source)])
|
|
defaultActiveDescendantHandler.assert_not_called()
|
|
if expectedOwner == "selection":
|
|
defaultSelectionHandler.assert_called_once_with(testScript, events[-1])
|
|
defaultFocusHandler.assert_not_called()
|
|
else:
|
|
defaultSelectionHandler.assert_not_called()
|
|
defaultFocusHandler.assert_called_once_with(testScript, events[-1])
|
|
|
|
def test_combo_box_active_descendant_then_focus_uses_focus_owner(self) -> None:
|
|
comboBox = FakeAccessible(Atspi.Role.COMBO_BOX)
|
|
option = FakeAccessible(Atspi.Role.LIST_ITEM)
|
|
events = [
|
|
FakeEvent("object:active-descendant-changed", comboBox, any_data=option),
|
|
FakeEvent("object:state-changed:focused", comboBox, detail1=1),
|
|
]
|
|
|
|
self._run_container_sequence(events, expectedOwner="focus")
|
|
|
|
def test_listbox_active_descendant_then_selection_uses_selection_owner(self) -> None:
|
|
listBox = FakeAccessible(Atspi.Role.LIST_BOX)
|
|
option = FakeAccessible(Atspi.Role.LIST_ITEM)
|
|
events = [
|
|
FakeEvent("object:active-descendant-changed", listBox, any_data=option),
|
|
FakeEvent("object:selection-changed", listBox),
|
|
]
|
|
|
|
self._run_container_sequence(events, expectedOwner="selection")
|
|
|
|
def test_tree_active_descendant_then_focus_uses_focus_owner(self) -> None:
|
|
tree = FakeAccessible(Atspi.Role.TREE)
|
|
treeItem = FakeAccessible(Atspi.Role.TREE_ITEM)
|
|
events = [
|
|
FakeEvent("object:active-descendant-changed", tree, any_data=treeItem),
|
|
FakeEvent("object:state-changed:focused", treeItem, detail1=1),
|
|
]
|
|
|
|
self._run_container_sequence(events, expectedOwner="focus")
|
|
|
|
def test_grid_active_descendant_then_selection_uses_selection_owner(self) -> None:
|
|
grid = FakeAccessible(Atspi.Role.TABLE)
|
|
gridCell = FakeAccessible(Atspi.Role.TABLE_CELL)
|
|
events = [
|
|
FakeEvent("object:active-descendant-changed", grid, any_data=gridCell),
|
|
FakeEvent("object:selection-changed", grid),
|
|
]
|
|
|
|
self._run_container_sequence(events, expectedOwner="selection")
|
|
|
|
def test_dialog_focus_loss_then_gain_uses_shared_focus_owner(self) -> None:
|
|
document = object()
|
|
oldFocus = object()
|
|
dialog = FakeAccessible(Atspi.Role.DIALOG)
|
|
testScript = self._make_script(document, oldFocus)
|
|
events = [
|
|
FakeEvent("object:state-changed:focused", dialog, detail1=0),
|
|
FakeEvent("object:state-changed:focused", dialog, detail1=1),
|
|
]
|
|
presentations: list[FakeAccessible] = []
|
|
|
|
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",
|
|
side_effect=lambda obj: obj is dialog,
|
|
),
|
|
mock.patch(
|
|
"cthulhu.scripts.web.script.cthulhu.setLocusOfFocus",
|
|
side_effect=lambda _event, obj: presentations.append(obj),
|
|
) as setLocusOfFocus,
|
|
mock.patch.object(default.Script, "onFocusedChanged") as defaultFocusHandler,
|
|
):
|
|
WebEventSequence(testScript).run(events)
|
|
|
|
self.assertEqual(presentations, [dialog])
|
|
setLocusOfFocus.assert_called_once_with(events[1], dialog)
|
|
defaultFocusHandler.assert_not_called()
|
|
|
|
|
|
class ChromiumImplementationDetailSequenceTests(unittest.TestCase):
|
|
"""Routing contracts for Chromium-only accessibility-tree details."""
|
|
|
|
@staticmethod
|
|
def _make_script(
|
|
staticTextLeaf: FakeAccessible | None = None,
|
|
listItemMarker: FakeAccessible | None = None,
|
|
) -> chromium_script.Script:
|
|
testScript = chromium_script.Script.__new__(chromium_script.Script)
|
|
testScript.utilities = mock.Mock()
|
|
testScript.utilities.isStaticTextLeaf.side_effect = (
|
|
lambda obj: obj is staticTextLeaf
|
|
)
|
|
testScript.utilities.isListItemMarker.side_effect = (
|
|
lambda obj: obj is listItemMarker
|
|
)
|
|
testScript.utilities.isRedundantAutocompleteEvent.return_value = False
|
|
return testScript
|
|
|
|
def test_static_text_child_caret_then_parent_caret_uses_parent_owner(self) -> None:
|
|
staticText = FakeAccessible(Atspi.Role.STATIC)
|
|
parentText = FakeAccessible(Atspi.Role.PARAGRAPH)
|
|
testScript = self._make_script(staticTextLeaf=staticText)
|
|
events = [
|
|
FakeEvent("object:text-caret-moved", staticText, detail1=0),
|
|
FakeEvent("object:text-caret-moved", parentText, detail1=0),
|
|
]
|
|
presentations: list[FakeAccessible] = []
|
|
|
|
with (
|
|
mock.patch.object(web_script.Script, "onCaretMoved", return_value=False) as webHandler,
|
|
mock.patch.object(
|
|
default.Script,
|
|
"onCaretMoved",
|
|
side_effect=lambda _script, event: presentations.append(event.source),
|
|
) as defaultHandler,
|
|
):
|
|
WebEventSequence(testScript).run(events)
|
|
|
|
self.assertEqual(presentations, [parentText])
|
|
webHandler.assert_called_once_with(events[1])
|
|
defaultHandler.assert_called_once_with(testScript, events[1])
|
|
|
|
|
|
def test_static_text_child_selection_then_parent_selection_uses_parent_owner(self) -> None:
|
|
staticText = FakeAccessible(Atspi.Role.STATIC)
|
|
parentText = FakeAccessible(Atspi.Role.PARAGRAPH)
|
|
testScript = self._make_script(staticTextLeaf=staticText)
|
|
events = [
|
|
FakeEvent("object:text-selection-changed", staticText),
|
|
FakeEvent("object:text-selection-changed", parentText),
|
|
]
|
|
|
|
self._assert_text_selection_owner(testScript, events, parentText)
|
|
|
|
def test_list_marker_selection_then_list_item_selection_uses_list_item_owner(self) -> None:
|
|
listMarker = FakeAccessible(Atspi.Role.STATIC)
|
|
listItem = FakeAccessible(Atspi.Role.LIST_ITEM)
|
|
testScript = self._make_script(listItemMarker=listMarker)
|
|
events = [
|
|
FakeEvent("object:text-selection-changed", listMarker),
|
|
FakeEvent("object:text-selection-changed", listItem),
|
|
]
|
|
|
|
self._assert_text_selection_owner(testScript, events, listItem)
|
|
|
|
def _assert_text_selection_owner(
|
|
self,
|
|
testScript: chromium_script.Script,
|
|
events: list[FakeEvent],
|
|
expectedOwner: FakeAccessible,
|
|
) -> None:
|
|
presentations: list[FakeAccessible] = []
|
|
|
|
with (
|
|
mock.patch.object(
|
|
web_script.Script,
|
|
"onTextSelectionChanged",
|
|
return_value=False,
|
|
) as webHandler,
|
|
mock.patch.object(
|
|
default.Script,
|
|
"onTextSelectionChanged",
|
|
side_effect=lambda _script, event: presentations.append(event.source),
|
|
) as defaultHandler,
|
|
):
|
|
WebEventSequence(testScript).run(events)
|
|
|
|
self.assertEqual(presentations, [expectedOwner])
|
|
webHandler.assert_called_once_with(events[1])
|
|
defaultHandler.assert_called_once_with(testScript, events[1])
|
|
|
|
|
|
class ChromiumMutationRecoverySequenceTests(unittest.TestCase):
|
|
"""Routing contracts for stale context replacement after child removal."""
|
|
|
|
@staticmethod
|
|
def _make_script(document: object, recoveredContext: FakeAccessible) -> chromium_script.Script:
|
|
testScript = chromium_script.Script.__new__(chromium_script.Script)
|
|
testScript._browseModeIsSticky = False
|
|
testScript._loadingDocumentContent = False
|
|
testScript.lastMouseRoutingTime = 0
|
|
testScript.utilities = mock.Mock()
|
|
testScript.utilities.isStaticTextLeaf.return_value = False
|
|
testScript.utilities.eventIsBrowserUINoise.return_value = False
|
|
testScript.utilities.isLiveRegion.return_value = False
|
|
testScript.utilities.getTopLevelDocumentForObject.return_value = document
|
|
testScript.utilities.getDocumentForObject.return_value = document
|
|
testScript.utilities.inDocumentContent.return_value = True
|
|
testScript.utilities.isZombie.return_value = False
|
|
testScript.utilities.isWebAppDescendant.return_value = False
|
|
testScript.utilities.handleEventForRemovedChild.return_value = False
|
|
testScript.utilities.handleEventFromContextReplicant.side_effect = (
|
|
lambda _event, candidate: candidate is recoveredContext
|
|
)
|
|
return testScript
|
|
|
|
def _assert_removed_context_then_replicant_recovers_once(
|
|
self,
|
|
recoveryEventType: str,
|
|
) -> None:
|
|
document = object()
|
|
container = FakeAccessible(Atspi.Role.PANEL)
|
|
removedContext = FakeAccessible(Atspi.Role.PUSH_BUTTON)
|
|
recoveredContext = FakeAccessible(Atspi.Role.PUSH_BUTTON)
|
|
testScript = self._make_script(document, recoveredContext)
|
|
removalEvent = FakeEvent(
|
|
"object:children-changed:remove",
|
|
container,
|
|
detail1=0,
|
|
any_data=removedContext,
|
|
)
|
|
if recoveryEventType == "object:children-changed:add":
|
|
recoveryEvent = FakeEvent(
|
|
recoveryEventType,
|
|
container,
|
|
detail1=0,
|
|
any_data=recoveredContext,
|
|
)
|
|
else:
|
|
recoveryEvent = FakeEvent(recoveryEventType, recoveredContext, detail1=1)
|
|
|
|
with (
|
|
mock.patch.object(cthulhu_state, "locusOfFocus", removedContext),
|
|
mock.patch.object(web_script.AXObject, "clear_cache_now"),
|
|
mock.patch.object(web_script.AXObject, "is_dead", return_value=True),
|
|
mock.patch.object(web_script.AXUtilities, "is_busy", return_value=False),
|
|
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(default.Script, "onChildrenRemoved") as defaultRemovalHandler,
|
|
mock.patch.object(default.Script, "onChildrenAdded") as defaultAddedHandler,
|
|
mock.patch.object(default.Script, "onFocusedChanged") as defaultFocusHandler,
|
|
):
|
|
WebEventSequence(testScript).run([removalEvent, recoveryEvent])
|
|
|
|
defaultRemovalHandler.assert_called_once_with(testScript, removalEvent)
|
|
testScript.utilities.handleEventForRemovedChild.assert_called_once_with(removalEvent)
|
|
testScript.utilities.handleEventFromContextReplicant.assert_called_once_with(
|
|
recoveryEvent,
|
|
recoveredContext,
|
|
)
|
|
defaultAddedHandler.assert_not_called()
|
|
defaultFocusHandler.assert_not_called()
|
|
|
|
def test_removed_context_then_added_replicant_uses_replicant_owner(self) -> None:
|
|
self._assert_removed_context_then_replicant_recovers_once(
|
|
"object:children-changed:add"
|
|
)
|
|
|
|
def test_removed_context_then_focused_replicant_uses_replicant_owner(self) -> None:
|
|
self._assert_removed_context_then_replicant_recovers_once(
|
|
"object:state-changed:focused"
|
|
)
|
|
|
|
|
|
class StickyModeTransitionSequenceTests(unittest.TestCase):
|
|
"""Ordered document-exit and re-entry contracts for sticky modes."""
|
|
|
|
@staticmethod
|
|
def _make_script(
|
|
document: object,
|
|
documentObjects: set[FakeAccessible],
|
|
inFocusMode: bool,
|
|
focusModeIsSticky: bool,
|
|
browseModeIsSticky: bool,
|
|
) -> chromium_script.Script:
|
|
testScript = chromium_script.Script.__new__(chromium_script.Script)
|
|
testScript._lastCommandWasCaretNav = False
|
|
testScript._lastCommandWasStructNav = False
|
|
testScript._lastCommandWasMouseButton = False
|
|
testScript._madeFindAnnouncement = False
|
|
testScript._inFocusMode = inFocusMode
|
|
testScript._focusModeIsSticky = focusModeIsSticky
|
|
testScript._browseModeIsSticky = browseModeIsSticky
|
|
testScript._navSuspended = False
|
|
testScript.refreshKeyGrabs = mock.Mock()
|
|
testScript.presentMessage = mock.Mock()
|
|
testScript.utilities = mock.Mock()
|
|
testScript.utilities.isZombie.return_value = False
|
|
testScript.utilities.isDocument.return_value = False
|
|
testScript.utilities.getTopLevelDocumentForObject.side_effect = (
|
|
lambda obj: document if obj in documentObjects else None
|
|
)
|
|
testScript.utilities.getDocumentForObject.side_effect = (
|
|
lambda obj: document if obj in documentObjects else None
|
|
)
|
|
testScript.utilities.inDocumentContent.side_effect = (
|
|
lambda obj: obj in documentObjects
|
|
)
|
|
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.shouldInterruptForLocusOfFocusChange.return_value = False
|
|
testScript.utilities.isWebAppDescendant.return_value = False
|
|
testScript.flatReviewPresenter = mock.Mock()
|
|
testScript.flatReviewPresenter.is_active.return_value = False
|
|
testScript.updateBraille = mock.Mock()
|
|
testScript.useFocusMode = mock.Mock(return_value=False)
|
|
testScript.speechGenerator = mock.Mock()
|
|
testScript.speechGenerator.generateSpeech.return_value = []
|
|
testScript._saveFocusedObjectInfo = mock.Mock()
|
|
|
|
def set_navigation_suspended(suspended: bool, _reason: str) -> None:
|
|
testScript._navSuspended = suspended
|
|
|
|
testScript._setNavigationSuspended = mock.Mock(
|
|
side_effect=set_navigation_suspended
|
|
)
|
|
return testScript
|
|
|
|
@staticmethod
|
|
def _run_round_trip(
|
|
testScript: chromium_script.Script,
|
|
documentControl: FakeAccessible,
|
|
browserUI: FakeAccessible,
|
|
returnedControl: FakeAccessible,
|
|
) -> list[tuple[bool, bool, bool, bool]]:
|
|
snapshots: list[tuple[bool, bool, bool, bool]] = []
|
|
transitions = [
|
|
(documentControl, browserUI),
|
|
(browserUI, returnedControl),
|
|
]
|
|
for oldFocus, newFocus in transitions:
|
|
web_script.Script.locus_of_focus_changed(
|
|
testScript,
|
|
None,
|
|
oldFocus,
|
|
newFocus,
|
|
)
|
|
snapshots.append(
|
|
(
|
|
testScript._inFocusMode,
|
|
testScript._focusModeIsSticky,
|
|
testScript._browseModeIsSticky,
|
|
testScript._navSuspended,
|
|
)
|
|
)
|
|
return snapshots
|
|
|
|
def test_sticky_focus_survives_document_exit_and_reentry(self) -> None:
|
|
document = object()
|
|
documentControl = FakeAccessible(Atspi.Role.ENTRY, editable=True)
|
|
browserUI = FakeAccessible(Atspi.Role.ENTRY, editable=True)
|
|
returnedControl = FakeAccessible(Atspi.Role.ENTRY, editable=True)
|
|
testScript = self._make_script(
|
|
document,
|
|
{documentControl, returnedControl},
|
|
inFocusMode=True,
|
|
focusModeIsSticky=True,
|
|
browseModeIsSticky=False,
|
|
)
|
|
|
|
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"),
|
|
mock.patch.object(web_script.cthulhu, "emitRegionChanged"),
|
|
):
|
|
snapshots = self._run_round_trip(
|
|
testScript,
|
|
documentControl,
|
|
browserUI,
|
|
returnedControl,
|
|
)
|
|
|
|
self.assertEqual(
|
|
snapshots,
|
|
[
|
|
(True, True, False, True),
|
|
(True, True, False, False),
|
|
],
|
|
)
|
|
testScript._setNavigationSuspended.assert_has_calls(
|
|
[
|
|
mock.call(True, "focus left document content"),
|
|
mock.call(False, "focus entered document content"),
|
|
]
|
|
)
|
|
|
|
def test_sticky_browse_survives_round_trip_and_blocks_widget_focus(self) -> None:
|
|
document = object()
|
|
documentControl = FakeAccessible(Atspi.Role.PARAGRAPH)
|
|
browserUI = FakeAccessible(Atspi.Role.ENTRY, editable=True)
|
|
webAppWidget = FakeAccessible(Atspi.Role.PUSH_BUTTON)
|
|
testScript = self._make_script(
|
|
document,
|
|
{documentControl, webAppWidget},
|
|
inFocusMode=False,
|
|
focusModeIsSticky=False,
|
|
browseModeIsSticky=True,
|
|
)
|
|
testScript.utilities.isWebAppDescendant.side_effect = (
|
|
lambda obj: obj is webAppWidget
|
|
)
|
|
focusEvent = FakeEvent(
|
|
"object:state-changed:focused",
|
|
webAppWidget,
|
|
detail1=1,
|
|
)
|
|
|
|
with (
|
|
mock.patch.object(cthulhu_state, "locusOfFocus", browserUI),
|
|
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"),
|
|
mock.patch.object(web_script.cthulhu, "emitRegionChanged"),
|
|
mock.patch.object(web_script.cthulhu, "setLocusOfFocus") as setLocusOfFocus,
|
|
mock.patch.object(default.Script, "onFocusedChanged") as defaultFocusHandler,
|
|
):
|
|
snapshots = self._run_round_trip(
|
|
testScript,
|
|
documentControl,
|
|
browserUI,
|
|
webAppWidget,
|
|
)
|
|
WebEventSequence(testScript).run([focusEvent])
|
|
|
|
self.assertEqual(
|
|
snapshots,
|
|
[
|
|
(False, False, True, True),
|
|
(False, False, True, False),
|
|
],
|
|
)
|
|
self.assertFalse(testScript._inFocusMode)
|
|
self.assertFalse(testScript._focusModeIsSticky)
|
|
self.assertTrue(testScript._browseModeIsSticky)
|
|
setLocusOfFocus.assert_not_called()
|
|
defaultFocusHandler.assert_not_called()
|
|
|
|
|
|
class DocumentAndBrowserUIRoundTripSequenceTests(unittest.TestCase):
|
|
"""Lifecycle and browser-UI round-trip contracts for web document context."""
|
|
|
|
def test_script_deactivate_then_reactivate_restores_document_navigation(self) -> None:
|
|
testScript = chromium_script.Script.__new__(chromium_script.Script)
|
|
app = object()
|
|
document = object()
|
|
focus = FakeAccessible(Atspi.Role.PARAGRAPH)
|
|
testScript.app = app
|
|
testScript._sayAllContents = [object()]
|
|
testScript._inSayAll = True
|
|
testScript._sayAllIsInterrupted = True
|
|
testScript._loadingDocumentContent = True
|
|
testScript._madeFindAnnouncement = True
|
|
testScript._lastCommandWasCaretNav = True
|
|
testScript._lastCommandWasStructNav = True
|
|
testScript._lastCommandWasMouseButton = True
|
|
testScript._lastMouseButtonContext = (object(), 3)
|
|
testScript._lastMouseOverObject = object()
|
|
testScript._preMouseOverContext = (object(), 4)
|
|
testScript._inMouseOverObject = True
|
|
testScript.utilities = mock.Mock()
|
|
testScript.utilities.getDocumentForObject.return_value = document
|
|
testScript._setNavigationSuspended = mock.Mock()
|
|
testScript.removeKeyGrabs = mock.Mock()
|
|
|
|
with (
|
|
mock.patch.object(cthulhu_state, "locusOfFocus", focus),
|
|
mock.patch.object(web_script.AXObject, "is_dead", return_value=False),
|
|
mock.patch.object(web_script.AXObject, "get_application", return_value=app),
|
|
mock.patch.object(default.Script, "activate") as defaultActivate,
|
|
):
|
|
web_script.Script.deactivate(testScript)
|
|
web_script.Script.activate(testScript)
|
|
|
|
self.assertEqual(testScript._sayAllContents, [])
|
|
self.assertFalse(testScript._inSayAll)
|
|
self.assertFalse(testScript._sayAllIsInterrupted)
|
|
self.assertFalse(testScript._loadingDocumentContent)
|
|
self.assertFalse(testScript._madeFindAnnouncement)
|
|
self.assertFalse(testScript._lastCommandWasCaretNav)
|
|
self.assertFalse(testScript._lastCommandWasStructNav)
|
|
self.assertFalse(testScript._lastCommandWasMouseButton)
|
|
self.assertEqual(testScript._lastMouseButtonContext, (None, -1))
|
|
self.assertIsNone(testScript._lastMouseOverObject)
|
|
self.assertEqual(testScript._preMouseOverContext, (None, -1))
|
|
self.assertFalse(testScript._inMouseOverObject)
|
|
testScript.utilities.clearCachedObjects.assert_called_once_with()
|
|
testScript._setNavigationSuspended.assert_has_calls(
|
|
[
|
|
mock.call(False, "script deactivation"),
|
|
mock.call(False, "activation-in-document"),
|
|
]
|
|
)
|
|
testScript.removeKeyGrabs.assert_called_once_with()
|
|
defaultActivate.assert_called_once_with()
|
|
|
|
def test_address_bar_round_trip_has_one_owner_per_focus_and_recovers_context(
|
|
self,
|
|
) -> None:
|
|
document = object()
|
|
pageControl = FakeAccessible(Atspi.Role.ENTRY, editable=True)
|
|
addressBar = FakeAccessible(Atspi.Role.ENTRY, editable=True)
|
|
returnedControl = FakeAccessible(Atspi.Role.ENTRY, editable=True)
|
|
testScript = StickyModeTransitionSequenceTests._make_script(
|
|
document,
|
|
{pageControl, returnedControl},
|
|
inFocusMode=False,
|
|
focusModeIsSticky=False,
|
|
browseModeIsSticky=False,
|
|
)
|
|
focusEvents = [
|
|
FakeEvent("object:state-changed:focused", addressBar, detail1=1),
|
|
FakeEvent("object:state-changed:focused", returnedControl, detail1=1),
|
|
]
|
|
presentations: list[FakeAccessible] = []
|
|
|
|
def present_and_move_focus(
|
|
_script: chromium_script.Script,
|
|
event: FakeEvent,
|
|
) -> None:
|
|
oldFocus = cthulhu_state.locusOfFocus
|
|
presentations.append(event.source)
|
|
web_script.Script.locus_of_focus_changed(
|
|
testScript,
|
|
event,
|
|
oldFocus,
|
|
event.source,
|
|
)
|
|
cthulhu_state.locusOfFocus = event.source
|
|
|
|
with (
|
|
mock.patch.object(cthulhu_state, "locusOfFocus", pageControl),
|
|
mock.patch.object(web_script.AXObject, "is_dead", return_value=False),
|
|
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_unknown_or_redundant", return_value=False),
|
|
mock.patch.object(web_script.AXUtilities, "is_heading", return_value=False),
|
|
mock.patch.object(web_script.speech, "speak"),
|
|
mock.patch.object(web_script.cthulhu, "emitRegionChanged"),
|
|
mock.patch.object(
|
|
default.Script,
|
|
"onFocusedChanged",
|
|
side_effect=present_and_move_focus,
|
|
) as defaultFocusHandler,
|
|
):
|
|
WebEventSequence(testScript).run(focusEvents)
|
|
|
|
self.assertEqual(presentations, [addressBar, returnedControl])
|
|
self.assertEqual(
|
|
defaultFocusHandler.call_args_list,
|
|
[
|
|
mock.call(testScript, focusEvents[0]),
|
|
mock.call(testScript, focusEvents[1]),
|
|
],
|
|
)
|
|
testScript._setNavigationSuspended.assert_has_calls(
|
|
[
|
|
mock.call(True, "focus left document content"),
|
|
mock.call(False, "focus entered document content"),
|
|
]
|
|
)
|
|
self.assertFalse(testScript._navSuspended)
|
|
testScript.utilities.setCaretContext.assert_called_once_with(
|
|
returnedControl,
|
|
0,
|
|
document,
|
|
)
|
|
testScript.updateBraille.assert_called_once_with(
|
|
returnedControl,
|
|
documentFrame=document,
|
|
)
|
|
|
|
|
|
class ChromiumDocumentLifecycleSequenceTests(unittest.TestCase):
|
|
"""Routing contracts for transient and authoritative Chromium documents."""
|
|
|
|
@staticmethod
|
|
def _make_script(
|
|
transientDocument: FakeAccessible,
|
|
realDocument: FakeAccessible,
|
|
) -> chromium_script.Script:
|
|
testScript = chromium_script.Script.__new__(chromium_script.Script)
|
|
testScript.utilities = mock.Mock()
|
|
testScript.utilities.hasNoSize.return_value = False
|
|
testScript.utilities.documentFrameURI.side_effect = (
|
|
lambda obj: "" if obj is transientDocument else "https://example.test/"
|
|
)
|
|
return testScript
|
|
|
|
def _assert_real_document_owns_sequence(
|
|
self,
|
|
eventType: str,
|
|
webHandlerName: str,
|
|
defaultHandlerName: str,
|
|
detail1: int = 0,
|
|
) -> None:
|
|
transientDocument = FakeAccessible(Atspi.Role.DOCUMENT_WEB)
|
|
realDocument = FakeAccessible(Atspi.Role.DOCUMENT_WEB)
|
|
testScript = self._make_script(transientDocument, realDocument)
|
|
events = [
|
|
FakeEvent(eventType, transientDocument, detail1=detail1),
|
|
FakeEvent(eventType, realDocument, detail1=detail1),
|
|
]
|
|
handledDocuments: list[FakeAccessible] = []
|
|
|
|
def handle_real_document(event: FakeEvent) -> bool:
|
|
handledDocuments.append(event.source)
|
|
return True
|
|
|
|
with (
|
|
mock.patch.object(
|
|
web_script.Script,
|
|
webHandlerName,
|
|
side_effect=handle_real_document,
|
|
) as webHandler,
|
|
mock.patch.object(default.Script, defaultHandlerName) as defaultHandler,
|
|
):
|
|
WebEventSequence(testScript).run(events)
|
|
|
|
self.assertEqual(handledDocuments, [realDocument])
|
|
webHandler.assert_called_once_with(events[1])
|
|
defaultHandler.assert_not_called()
|
|
|
|
def test_uri_less_busy_then_real_busy_uses_real_document_owner(self) -> None:
|
|
self._assert_real_document_owns_sequence(
|
|
"object:state-changed:busy",
|
|
"onBusyChanged",
|
|
"onBusyChanged",
|
|
detail1=1,
|
|
)
|
|
|
|
def test_uri_less_load_complete_then_real_load_complete_uses_real_document_owner(
|
|
self,
|
|
) -> None:
|
|
self._assert_real_document_owns_sequence(
|
|
"document:load-complete",
|
|
"onDocumentLoadComplete",
|
|
"onDocumentLoadComplete",
|
|
)
|
|
|
|
def test_uri_less_reload_then_real_reload_uses_real_document_owner(self) -> None:
|
|
self._assert_real_document_owns_sequence(
|
|
"document:reload",
|
|
"onDocumentReload",
|
|
"onDocumentReload",
|
|
)
|
|
|
|
|
|
class GeckoFocusAndFindSequenceTests(unittest.TestCase):
|
|
"""Routing contracts for Gecko focus noise and find-result events."""
|
|
|
|
@staticmethod
|
|
def _make_script() -> gecko_script.Script:
|
|
testScript = gecko_script.Script.__new__(gecko_script.Script)
|
|
testScript._lastCommandWasCaretNav = False
|
|
testScript._lastCommandWasStructNav = False
|
|
testScript._lastCommandWasMouseButton = False
|
|
testScript._clearSyntheticWebSelection = mock.Mock()
|
|
testScript._saveFocusedObjectInfo = mock.Mock()
|
|
testScript.presentFindResults = mock.Mock()
|
|
testScript.utilities = mock.Mock()
|
|
testScript.utilities.isZombie.return_value = False
|
|
testScript.utilities.getTopLevelDocumentForObject.return_value = object()
|
|
testScript.utilities.getCaretContext.return_value = (object(), 0)
|
|
testScript.utilities.lastInputEventWasCaretNavWithSelection.return_value = False
|
|
testScript.utilities.lastInputEventWasCharNav.return_value = False
|
|
testScript.utilities.inFindContainer.return_value = True
|
|
return testScript
|
|
|
|
def _assert_focus_noise_then_control_uses_control_owner(
|
|
self,
|
|
noiseRole: Atspi.Role,
|
|
) -> None:
|
|
activeWindow = FakeAccessible(Atspi.Role.FRAME)
|
|
noise = FakeAccessible(noiseRole)
|
|
control = FakeAccessible(Atspi.Role.ENTRY, editable=True)
|
|
testScript = self._make_script()
|
|
events = [
|
|
FakeEvent("object:state-changed:focused", noise, detail1=1),
|
|
FakeEvent("object:state-changed:focused", control, detail1=1),
|
|
]
|
|
presentations: list[FakeAccessible] = []
|
|
|
|
with (
|
|
mock.patch.object(cthulhu_state, "activeWindow", activeWindow),
|
|
mock.patch.object(cthulhu_state, "locusOfFocus", activeWindow),
|
|
mock.patch.object(web_script.Script, "onFocusedChanged", return_value=False) as webHandler,
|
|
mock.patch.object(
|
|
gecko_script.AXObject,
|
|
"get_role",
|
|
side_effect=lambda obj: obj.role,
|
|
),
|
|
mock.patch.object(
|
|
default.Script,
|
|
"onFocusedChanged",
|
|
side_effect=lambda _script, event: presentations.append(event.source),
|
|
) as defaultHandler,
|
|
):
|
|
GeckoEventSequence(testScript).run(events)
|
|
|
|
self.assertEqual(presentations, [control])
|
|
self.assertEqual(webHandler.call_args_list, [mock.call(events[0]), mock.call(events[1])])
|
|
defaultHandler.assert_called_once_with(testScript, events[1])
|
|
|
|
def test_panel_focus_noise_then_real_focus_uses_real_control_owner(self) -> None:
|
|
self._assert_focus_noise_then_control_uses_control_owner(Atspi.Role.PANEL)
|
|
|
|
def test_frame_focus_noise_then_real_focus_uses_real_control_owner(self) -> None:
|
|
self._assert_focus_noise_then_control_uses_control_owner(Atspi.Role.FRAME)
|
|
|
|
def test_find_entry_focus_then_result_caret_uses_find_presenter(self) -> None:
|
|
findEntry = FakeAccessible(Atspi.Role.ENTRY, editable=True)
|
|
resultText = FakeAccessible(Atspi.Role.PARAGRAPH)
|
|
testScript = self._make_script()
|
|
events = [
|
|
FakeEvent("object:state-changed:focused", findEntry, detail1=1),
|
|
FakeEvent("object:text-caret-moved", resultText, detail1=7),
|
|
]
|
|
focusPresentations: list[FakeAccessible] = []
|
|
|
|
with (
|
|
mock.patch.object(cthulhu_state, "activeWindow", FakeAccessible(Atspi.Role.FRAME)),
|
|
mock.patch.object(cthulhu_state, "locusOfFocus", findEntry),
|
|
mock.patch.object(web_script.Script, "onFocusedChanged", return_value=False),
|
|
mock.patch.object(
|
|
gecko_script.AXObject,
|
|
"get_role",
|
|
side_effect=lambda obj: obj.role,
|
|
),
|
|
mock.patch.object(
|
|
default.Script,
|
|
"onFocusedChanged",
|
|
side_effect=lambda _script, event: focusPresentations.append(event.source),
|
|
) as defaultFocusHandler,
|
|
mock.patch.object(default.Script, "onCaretMoved") as defaultCaretHandler,
|
|
mock.patch.object(AXUtilitiesEvent, "get_text_event_reason", return_value=None),
|
|
):
|
|
GeckoEventSequence(testScript).run(events)
|
|
|
|
self.assertEqual(focusPresentations, [findEntry])
|
|
defaultFocusHandler.assert_called_once_with(testScript, events[0])
|
|
testScript.presentFindResults.assert_called_once_with(resultText, 7)
|
|
testScript._saveFocusedObjectInfo.assert_called_once_with(findEntry)
|
|
defaultCaretHandler.assert_not_called()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|