More web optimizations.
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user