More web optimizations.

This commit is contained in:
Storm Dragon
2026-07-22 13:26:18 -04:00
parent 1249aa791c
commit 17f196e530
2 changed files with 373 additions and 5 deletions
+22 -5
View File
@@ -173,8 +173,12 @@ class Script(default.Script):
obj,
offset,
focusOverride=None,
):
if not self.utilities.lastInputEventWasCaretNavWithSelection():
isCaretSelection: bool | None = None,
) -> bool:
if isCaretSelection is None:
isCaretSelection = self.utilities.lastInputEventWasCaretNavWithSelection()
if not isCaretSelection:
return False
if focusOverride is not None:
@@ -3181,6 +3185,8 @@ class Script(default.Script):
debug.printMessage(debug.LEVEL_INFO, msg, True)
return False
reason = AXUtilitiesEvent.get_text_event_reason(event)
if not self.utilities.inDocumentContent(cthulhu_state.locusOfFocus):
tokens = ["WEB: Event ignored: locusOfFocus", cthulhu_state.locusOfFocus,
"is not in document content"]
@@ -3208,8 +3214,18 @@ class Script(default.Script):
debug.printMessage(debug.LEVEL_INFO, msg, True)
return True
if self.utilities.lastInputEventWasCaretNavWithSelection() \
and not AXText.get_selected_ranges(event.source):
selectionReasons = (
TextEventReason.SELECTION_BY_CHARACTER,
TextEventReason.SELECTION_BY_LINE,
TextEventReason.SELECTION_BY_PARAGRAPH,
TextEventReason.SELECTION_BY_PAGE,
TextEventReason.SELECTION_BY_WORD,
TextEventReason.SELECTION_TO_FILE_BOUNDARY,
TextEventReason.SELECTION_TO_LINE_BOUNDARY,
TextEventReason.UNSPECIFIED_SELECTION,
)
isSelectionReason = reason in selectionReasons
if isSelectionReason and not AXText.get_selected_ranges(event.source):
document = self.utilities.getTopLevelDocumentForObject(event.source)
obj, offset = self.utilities.getCaretContext(document, False, False)
focusOffset = AXText.get_caret_offset(event.source)
@@ -3219,6 +3235,7 @@ class Script(default.Script):
obj,
offset,
focusOverride=(event.source, focusOffset),
isCaretSelection=True,
):
msg = "WEB: Event handled: synthetic selection from event source"
debug.printMessage(debug.LEVEL_INFO, msg, True)
@@ -3239,7 +3256,7 @@ class Script(default.Script):
offset = AXText.get_caret_offset(event.source)
char = AXText.get_substring(event.source, offset, offset + 1)
if char == self.EMBEDDED_OBJECT_CHARACTER \
and not self.utilities.lastInputEventWasCaretNavWithSelection() \
and not isSelectionReason \
and not self.utilities.lastInputEventWasCommand():
msg = "WEB: Ignoring: Not selecting and event offset is at embedded object"
debug.printMessage(debug.LEVEL_INFO, msg, True)
+351
View File
@@ -529,11 +529,63 @@ class TextEventReasonRegressionTests(unittest.TestCase):
"last_event_was_middle_release",
"last_event_was_up_or_down",
"last_event_was_page_up_or_page_down",
"last_event_was_caret_selection",
"last_event_was_caret_navigation",
"last_event_was_line_navigation",
"last_event_was_word_navigation",
"last_event_was_character_navigation",
"last_event_was_page_navigation",
"last_event_was_line_boundary_navigation",
"last_event_was_file_boundary_navigation",
"last_event_was_select_all",
"last_event_was_primary_click_or_release",
)
for method in methods:
getattr(manager, method).return_value = False
return manager
def _classify_selection(
self,
event,
manager,
*,
focus=None,
focusIsSearch=False,
isEditable=False,
spinAncestor=None,
):
with (
mock.patch("cthulhu.input_event_manager.get_manager", return_value=manager),
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=focusIsSearch,
),
mock.patch(
"cthulhu.ax_utilities_event.AXUtilitiesState.is_editable",
return_value=isEditable,
),
mock.patch(
"cthulhu.ax_utilities_event.AXUtilitiesRole.is_terminal",
return_value=False,
),
mock.patch(
"cthulhu.ax_utilities_event.AXUtilitiesRole.is_spin_button",
return_value=False,
),
mock.patch.object(
web_script.AXObject,
"find_ancestor",
return_value=spinAncestor,
),
):
getFocusManager.return_value.get_locus_of_focus.return_value = (
event.source if focus is None else focus
)
return AXUtilitiesEvent._get_text_selection_changed_event_reason(event)
def _classify_insertion(
self,
event,
@@ -764,6 +816,92 @@ class TextEventReasonRegressionTests(unittest.TestCase):
self.assertEqual(reason, expected)
def test_selection_classifier_covers_handler_reason_families(self):
source = object()
cases = (
(
"search presentable",
TextEventReason.SEARCH_PRESENTABLE,
{"focus": object(), "focusIsSearch": True},
{},
),
(
"search unpresentable",
TextEventReason.SEARCH_UNPRESENTABLE,
{"focus": object(), "focusIsSearch": True},
{"last_event_was_backspace": True},
),
(
"selection by word",
TextEventReason.SELECTION_BY_WORD,
{},
{
"last_event_was_caret_selection": True,
"last_event_was_word_navigation": True,
},
),
(
"unspecified selection",
TextEventReason.UNSPECIFIED_SELECTION,
{},
{"last_event_was_caret_selection": True},
),
(
"navigation by character",
TextEventReason.NAVIGATION_BY_CHARACTER,
{},
{
"last_event_was_caret_navigation": True,
"last_event_was_character_navigation": True,
},
),
(
"select all",
TextEventReason.SELECT_ALL,
{},
{"last_event_was_select_all": True},
),
(
"primary mouse",
TextEventReason.MOUSE_PRIMARY_BUTTON,
{},
{"last_event_was_primary_click_or_release": True},
),
(
"typing",
TextEventReason.TYPING,
{"isEditable": True},
{"last_event_was_printable_key": True},
),
(
"editing command",
TextEventReason.PASTE,
{"isEditable": True},
{"last_event_was_paste": True},
),
(
"spin button",
TextEventReason.SPIN_BUTTON_VALUE_CHANGE,
{"isEditable": True, "spinAncestor": object()},
{"last_event_was_up_or_down": True},
),
)
for name, expected, classifierKwargs, managerResults in cases:
with self.subTest(name=name):
manager = self._make_input_manager()
for method, result in managerResults.items():
getattr(manager, method).return_value = result
event = mock.Mock(type="object:text-selection-changed", source=source)
reason = self._classify_selection(
event,
manager,
**classifierKwargs,
)
self.assertEqual(reason, expected)
def test_tab_is_classified_as_focus_change_before_editable_state(self):
oldFocus = object()
source = object()
@@ -1727,6 +1865,11 @@ class WebSelectionRegressionTests(unittest.TestCase):
with (
mock.patch.object(web_script.cthulhu_state, "locusOfFocus", anchor),
mock.patch.object(
web_script.AXUtilitiesEvent,
"get_text_event_reason",
return_value=TextEventReason.SELECTION_BY_CHARACTER,
),
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"),
@@ -1744,6 +1887,214 @@ class WebSelectionRegressionTests(unittest.TestCase):
"S",
)
def test_selection_reason_owns_synthetic_selection_recovery(self):
testScript = self._make_script()
event = mock.Mock(source=object())
testScript.utilities.textEventIsForNonNavigableTextObject.return_value = False
testScript.utilities.lastInputEventWasCaretNavWithSelection.return_value = False
testScript.utilities.isContentEditableWithEmbeddedObjects.return_value = False
testScript._presentSyntheticCaretSelection = mock.Mock(return_value=True)
testScript.utilities.getTopLevelDocumentForObject.return_value = object()
testScript.utilities.getCaretContext.return_value = (object(), 0)
with (
mock.patch.object(web_script.cthulhu_state, "locusOfFocus", object()),
mock.patch.object(
web_script.AXUtilitiesEvent,
"get_text_event_reason",
return_value=TextEventReason.SELECTION_BY_CHARACTER,
) as getReason,
mock.patch.object(web_script.AXText, "get_selected_ranges", return_value=[]),
mock.patch.object(web_script.AXText, "get_caret_offset", return_value=0),
mock.patch.object(web_script.AXText, "get_substring", return_value="x"),
mock.patch.object(web_script.AXUtilities, "is_text_input", return_value=False),
):
result = web_script.Script.onTextSelectionChanged(testScript, event)
self.assertTrue(result)
getReason.assert_called_once_with(event)
testScript._presentSyntheticCaretSelection.assert_called_once()
def test_navigation_reason_does_not_claim_synthetic_selection_recovery(self):
testScript = self._make_script()
event = mock.Mock(source=object())
testScript.utilities.textEventIsForNonNavigableTextObject.return_value = False
testScript.utilities.lastInputEventWasCaretNavWithSelection.return_value = True
testScript.utilities.isContentEditableWithEmbeddedObjects.return_value = False
testScript._presentSyntheticCaretSelection = mock.Mock(return_value=True)
testScript.utilities.getTopLevelDocumentForObject.return_value = object()
testScript.utilities.getCaretContext.return_value = (object(), 0)
with (
mock.patch.object(web_script.cthulhu_state, "locusOfFocus", object()),
mock.patch.object(
web_script.AXUtilitiesEvent,
"get_text_event_reason",
return_value=TextEventReason.NAVIGATION_BY_CHARACTER,
) as getReason,
mock.patch.object(web_script.AXText, "get_selected_ranges", return_value=[]),
mock.patch.object(web_script.AXText, "get_caret_offset", return_value=0),
mock.patch.object(web_script.AXText, "get_substring", return_value="x"),
mock.patch.object(web_script.AXUtilities, "is_text_input", return_value=False),
):
result = web_script.Script.onTextSelectionChanged(testScript, event)
self.assertFalse(result)
getReason.assert_called_once_with(event)
testScript._presentSyntheticCaretSelection.assert_not_called()
def test_search_reason_preserves_focus_outside_document_suppression(self):
testScript = self._make_script()
source = object()
focus = object()
event = mock.Mock(source=source)
testScript.utilities.inDocumentContent.side_effect = lambda obj: obj is source
with (
mock.patch.object(web_script.cthulhu_state, "locusOfFocus", focus),
mock.patch.object(
web_script.AXUtilitiesEvent,
"get_text_event_reason",
return_value=TextEventReason.SEARCH_PRESENTABLE,
) as getReason,
):
result = web_script.Script.onTextSelectionChanged(testScript, event)
self.assertTrue(result)
getReason.assert_called_once_with(event)
def test_editing_reason_preserves_default_selection_owner(self):
testScript = self._make_script()
source = object()
event = mock.Mock(source=source)
testScript.utilities.textEventIsForNonNavigableTextObject.return_value = False
testScript.utilities.lastInputEventWasCaretNavWithSelection.return_value = False
testScript.utilities.isContentEditableWithEmbeddedObjects.return_value = False
with (
mock.patch.object(web_script.cthulhu_state, "locusOfFocus", source),
mock.patch.object(
web_script.AXUtilitiesEvent,
"get_text_event_reason",
return_value=TextEventReason.PASTE,
) as getReason,
mock.patch.object(web_script.AXText, "get_caret_offset", return_value=0),
mock.patch.object(web_script.AXText, "get_substring", return_value="x"),
mock.patch.object(web_script.AXUtilities, "is_text_input", return_value=False),
):
result = web_script.Script.onTextSelectionChanged(testScript, event)
self.assertFalse(result)
getReason.assert_called_once_with(event)
def test_autocomplete_noise_still_precedes_reason_specific_ownership(self):
testScript = self._make_script()
event = mock.Mock(source=object())
testScript.utilities.eventIsAutocompleteNoise.return_value = True
with (
mock.patch.object(web_script.cthulhu_state, "locusOfFocus", object()),
mock.patch.object(
web_script.AXUtilitiesEvent,
"get_text_event_reason",
return_value=TextEventReason.SELECTION_BY_WORD,
) as getReason,
):
result = web_script.Script.onTextSelectionChanged(testScript, event)
self.assertTrue(result)
getReason.assert_called_once_with(event)
testScript.utilities.lastInputEventWasCaretNavWithSelection.assert_not_called()
def test_spinner_noise_still_precedes_reason_specific_ownership(self):
testScript = self._make_script()
event = mock.Mock(source=object())
testScript.utilities.eventIsSpinnerNoise.return_value = True
with (
mock.patch.object(web_script.cthulhu_state, "locusOfFocus", object()),
mock.patch.object(
web_script.AXUtilitiesEvent,
"get_text_event_reason",
return_value=TextEventReason.SPIN_BUTTON_VALUE_CHANGE,
) as getReason,
):
result = web_script.Script.onTextSelectionChanged(testScript, event)
self.assertTrue(result)
getReason.assert_called_once_with(event)
testScript.utilities.lastInputEventWasCaretNavWithSelection.assert_not_called()
def test_non_navigable_text_still_suppresses_selection_reason(self):
testScript = self._make_script()
event = mock.Mock(source=object())
testScript.utilities.textEventIsForNonNavigableTextObject.return_value = True
with (
mock.patch.object(web_script.cthulhu_state, "locusOfFocus", object()),
mock.patch.object(
web_script.AXUtilitiesEvent,
"get_text_event_reason",
return_value=TextEventReason.SELECTION_BY_LINE,
) as getReason,
):
result = web_script.Script.onTextSelectionChanged(testScript, event)
self.assertTrue(result)
getReason.assert_called_once_with(event)
testScript.utilities.lastInputEventWasCaretNavWithSelection.assert_not_called()
def test_selection_reason_at_embedded_object_offset_defers_to_default(self):
testScript = self._make_script()
source = object()
event = mock.Mock(source=source)
testScript.utilities.textEventIsForNonNavigableTextObject.return_value = False
testScript.utilities.lastInputEventWasCaretNavWithSelection.return_value = False
testScript.utilities.isContentEditableWithEmbeddedObjects.return_value = False
with (
mock.patch.object(web_script.cthulhu_state, "locusOfFocus", source),
mock.patch.object(
web_script.AXUtilitiesEvent,
"get_text_event_reason",
return_value=TextEventReason.SELECTION_BY_WORD,
),
mock.patch.object(web_script.AXText, "get_selected_ranges", return_value=[(0, 1)]),
mock.patch.object(web_script.AXText, "get_caret_offset", return_value=0),
mock.patch.object(
web_script.AXText,
"get_substring",
return_value=web_script.Script.EMBEDDED_OBJECT_CHARACTER,
),
mock.patch.object(web_script.AXUtilities, "is_text_input", return_value=False),
):
result = web_script.Script.onTextSelectionChanged(testScript, event)
self.assertFalse(result)
testScript.utilities.lastInputEventWasCaretNavWithSelection.assert_not_called()
def test_contenteditable_embedded_objects_still_defer_to_default(self):
testScript = self._make_script()
source = object()
event = mock.Mock(source=source)
testScript.utilities.textEventIsForNonNavigableTextObject.return_value = False
testScript.utilities.lastInputEventWasCaretNavWithSelection.return_value = False
testScript.utilities.isContentEditableWithEmbeddedObjects.return_value = True
with (
mock.patch.object(web_script.cthulhu_state, "locusOfFocus", source),
mock.patch.object(
web_script.AXUtilitiesEvent,
"get_text_event_reason",
return_value=TextEventReason.TYPING,
) as getReason,
mock.patch.object(web_script.AXUtilities, "is_text_input", return_value=False),
):
result = web_script.Script.onTextSelectionChanged(testScript, event)
self.assertFalse(result)
getReason.assert_called_once_with(event)
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)