Merge branch 'testing'
This commit is contained in:
@@ -870,6 +870,8 @@ class KeyboardEvent(InputEvent):
|
|||||||
return True, 'Cthulhu modifier'
|
return True, 'Cthulhu modifier'
|
||||||
|
|
||||||
if not self._handler:
|
if not self._handler:
|
||||||
|
if scriptConsumes:
|
||||||
|
return True, 'Script consumed without handler'
|
||||||
return False, 'No handler'
|
return False, 'No handler'
|
||||||
|
|
||||||
return scriptConsumes, 'Script indication'
|
return scriptConsumes, 'Script indication'
|
||||||
@@ -1044,7 +1046,7 @@ class KeyboardEvent(InputEvent):
|
|||||||
return False, 'Should not consume'
|
return False, 'Should not consume'
|
||||||
|
|
||||||
if not (self._consumer or self._handler):
|
if not (self._consumer or self._handler):
|
||||||
return False, 'No consumer or handler'
|
return True, 'Consumed during shouldConsume'
|
||||||
|
|
||||||
if self._consumer or self._handler.function:
|
if self._consumer or self._handler.function:
|
||||||
GLib.timeout_add(1, self._consume)
|
GLib.timeout_add(1, self._consume)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
steamwebhelper_python_sources = files([
|
steamwebhelper_python_sources = files([
|
||||||
'__init__.py',
|
'__init__.py',
|
||||||
'script.py',
|
'script.py',
|
||||||
|
'script_utilities.py',
|
||||||
])
|
])
|
||||||
|
|
||||||
python3.install_sources(
|
python3.install_sources(
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ from cthulhu.ax_utilities import AXUtilities
|
|||||||
from cthulhu.ax_utilities_relation import AXUtilitiesRelation
|
from cthulhu.ax_utilities_relation import AXUtilitiesRelation
|
||||||
from cthulhu.ax_utilities_role import AXUtilitiesRole
|
from cthulhu.ax_utilities_role import AXUtilitiesRole
|
||||||
from cthulhu.scripts.toolkits import Chromium
|
from cthulhu.scripts.toolkits import Chromium
|
||||||
|
from .script_utilities import Utilities
|
||||||
|
|
||||||
settingsManager = settings_manager.getManager()
|
settingsManager = settings_manager.getManager()
|
||||||
|
|
||||||
@@ -88,6 +89,13 @@ class Script(Chromium.Script):
|
|||||||
re.IGNORECASE
|
re.IGNORECASE
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def shouldConsumeKeyboardEvent(self, keyboardEvent, handler) -> bool:
|
||||||
|
consumes = super().shouldConsumeKeyboardEvent(keyboardEvent, handler)
|
||||||
|
if consumes:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return self._trySteamButtonActivation(keyboardEvent)
|
||||||
|
|
||||||
def onShowingChanged(self, event):
|
def onShowingChanged(self, event):
|
||||||
"""Callback for object:state-changed:showing accessibility events."""
|
"""Callback for object:state-changed:showing accessibility events."""
|
||||||
|
|
||||||
@@ -99,6 +107,9 @@ class Script(Chromium.Script):
|
|||||||
# Fall through to Chromium/web handling
|
# Fall through to Chromium/web handling
|
||||||
super().onShowingChanged(event)
|
super().onShowingChanged(event)
|
||||||
|
|
||||||
|
def getUtilities(self):
|
||||||
|
return Utilities(self)
|
||||||
|
|
||||||
def onChildrenAdded(self, event):
|
def onChildrenAdded(self, event):
|
||||||
"""Callback for object:children-changed:add accessibility events."""
|
"""Callback for object:children-changed:add accessibility events."""
|
||||||
|
|
||||||
@@ -154,6 +165,40 @@ class Script(Chromium.Script):
|
|||||||
self._logSteamNavigationEvent("active-descendant-changed", event)
|
self._logSteamNavigationEvent("active-descendant-changed", event)
|
||||||
return super().onActiveDescendantChanged(event)
|
return super().onActiveDescendantChanged(event)
|
||||||
|
|
||||||
|
def _trySteamButtonActivation(self, keyboardEvent) -> bool:
|
||||||
|
if keyboardEvent.event_string not in ["Return", "KP_Enter"]:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not keyboardEvent.is_pressed_key():
|
||||||
|
return False
|
||||||
|
|
||||||
|
if getattr(keyboardEvent, "modifiers", 0):
|
||||||
|
return False
|
||||||
|
|
||||||
|
obj = self._getClickableActivationTarget()
|
||||||
|
if not obj or not self.utilities.inDocumentContent(obj):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not (AXUtilities.is_button(obj) or AXUtilities.is_push_button(obj)):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not any(
|
||||||
|
AXObject.has_action(obj, actionName)
|
||||||
|
for actionName in ["press", "click", "click-ancestor", "activate", "open", "jump"]
|
||||||
|
):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self._performClickableAction(obj):
|
||||||
|
return True
|
||||||
|
|
||||||
|
from cthulhu import ax_event_synthesizer
|
||||||
|
result = ax_event_synthesizer.AXEventSynthesizer.click_object(obj)
|
||||||
|
if result:
|
||||||
|
self._restoreFocusAfterClick(obj)
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
def _isSteamNotification(self, obj):
|
def _isSteamNotification(self, obj):
|
||||||
"""Detect if object is a Steam notification.
|
"""Detect if object is a Steam notification.
|
||||||
|
|
||||||
@@ -253,29 +298,37 @@ class Script(Chromium.Script):
|
|||||||
return f"string('{text}')"
|
return f"string('{text}')"
|
||||||
if anyData is None:
|
if anyData is None:
|
||||||
return "None"
|
return "None"
|
||||||
|
if isinstance(anyData, (bool, int, float)):
|
||||||
|
return repr(anyData)
|
||||||
return self._describeSteamObject(anyData)
|
return self._describeSteamObject(anyData)
|
||||||
|
|
||||||
def _describeSteamObject(self, obj):
|
def _describeSteamObject(self, obj):
|
||||||
if obj is None:
|
if obj is None:
|
||||||
return "None"
|
return "None"
|
||||||
|
|
||||||
name = AXObject.get_name(obj) or ""
|
try:
|
||||||
description = AXObject.get_description(obj) or ""
|
name = AXObject.get_name(obj) or ""
|
||||||
roleName = AXObject.get_role_name(obj) or ""
|
description = AXObject.get_description(obj) or ""
|
||||||
text = self.utilities.displayedText(obj) or ""
|
roleName = AXObject.get_role_name(obj) or ""
|
||||||
path = AXObject.get_path(obj)
|
text = self.utilities.displayedText(obj) or ""
|
||||||
|
path = AXObject.get_path(obj)
|
||||||
|
|
||||||
name = self._normalizeSteamNotificationText(name)
|
name = self._normalizeSteamNotificationText(name)
|
||||||
description = self._normalizeSteamNotificationText(description)
|
description = self._normalizeSteamNotificationText(description)
|
||||||
text = self._normalizeSteamNotificationText(text)
|
text = self._normalizeSteamNotificationText(text)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
f"role='{roleName}' "
|
f"role='{roleName}' "
|
||||||
f"name='{name}' "
|
f"name='{name}' "
|
||||||
f"description='{description}' "
|
f"description='{description}' "
|
||||||
f"text='{text}' "
|
f"text='{text}' "
|
||||||
f"path={path}"
|
f"path={path}"
|
||||||
)
|
)
|
||||||
|
except Exception as error:
|
||||||
|
return (
|
||||||
|
f"uninspectable(type={type(obj).__name__}, "
|
||||||
|
f"value={obj!r}, error={error})"
|
||||||
|
)
|
||||||
|
|
||||||
def _presentSteamLiveRegionText(self, event):
|
def _presentSteamLiveRegionText(self, event):
|
||||||
if not isinstance(event.any_data, str):
|
if not isinstance(event.any_data, str):
|
||||||
|
|||||||
@@ -0,0 +1,162 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
#
|
||||||
|
# Copyright (c) 2024 Stormux
|
||||||
|
#
|
||||||
|
# This library is free software; you can redistribute it and/or
|
||||||
|
# modify it under the terms of the GNU Lesser General Public
|
||||||
|
# License as published by the Free Software Foundation; either
|
||||||
|
# version 2.1 of the License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This library is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
# Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Lesser General Public
|
||||||
|
# License along with this library; if not, write to the
|
||||||
|
# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
|
||||||
|
# Boston MA 02110-1301 USA.
|
||||||
|
#
|
||||||
|
# Cthulhu project: https://git.stormux.org/storm/cthulhu
|
||||||
|
|
||||||
|
"""Steam-specific utility helpers."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from cthulhu import debug
|
||||||
|
from cthulhu.ax_object import AXObject
|
||||||
|
from cthulhu.ax_text import AXText
|
||||||
|
from cthulhu.ax_utilities import AXUtilities
|
||||||
|
from cthulhu.scripts.toolkits.Chromium.script_utilities import Utilities as ChromiumUtilities
|
||||||
|
|
||||||
|
|
||||||
|
class Utilities(ChromiumUtilities):
|
||||||
|
def __init__(self, script) -> None:
|
||||||
|
super().__init__(script)
|
||||||
|
self._steamInferredButtonLabels: dict[int, str] = {}
|
||||||
|
|
||||||
|
def clearCachedObjects(self) -> None:
|
||||||
|
super().clearCachedObjects()
|
||||||
|
self._steamInferredButtonLabels = {}
|
||||||
|
|
||||||
|
def displayedLabel(self, obj):
|
||||||
|
label = super().displayedLabel(obj)
|
||||||
|
if label or not self._shouldInferSteamButtonLabel(obj):
|
||||||
|
return label
|
||||||
|
|
||||||
|
inferredLabel = self._getSteamInferredButtonLabel(obj)
|
||||||
|
if inferredLabel:
|
||||||
|
self._displayedLabelText[hash(obj)] = inferredLabel
|
||||||
|
return inferredLabel
|
||||||
|
|
||||||
|
def displayedText(self, obj):
|
||||||
|
text = super().displayedText(obj)
|
||||||
|
if text or not self._shouldInferSteamButtonLabel(obj):
|
||||||
|
return text
|
||||||
|
|
||||||
|
inferredLabel = self._getSteamInferredButtonLabel(obj)
|
||||||
|
if inferredLabel:
|
||||||
|
cache = self._script.generatorCache.setdefault(self.DISPLAYED_TEXT, {})
|
||||||
|
cache[obj] = inferredLabel
|
||||||
|
return inferredLabel
|
||||||
|
|
||||||
|
def _shouldInferSteamButtonLabel(self, obj) -> bool:
|
||||||
|
if not (obj and self.inDocumentContent(obj)):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if AXObject.get_name(obj):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not (AXUtilities.is_button(obj) or AXUtilities.is_push_button(obj)):
|
||||||
|
return False
|
||||||
|
|
||||||
|
className = AXObject.get_attribute(obj, "class") or ""
|
||||||
|
return "FriendsListTab" in className or "AddFriendButton" in className
|
||||||
|
|
||||||
|
def _getSteamInferredButtonLabel(self, obj) -> str:
|
||||||
|
cached = self._steamInferredButtonLabels.get(hash(obj))
|
||||||
|
if cached is not None:
|
||||||
|
return cached
|
||||||
|
|
||||||
|
className = AXObject.get_attribute(obj, "class") or ""
|
||||||
|
inferredLabel = self._getSteamButtonLabelFromClass(className)
|
||||||
|
if not inferredLabel:
|
||||||
|
inferredLabel = self._getSteamNearbyButtonLabel(obj)
|
||||||
|
|
||||||
|
inferredLabel = inferredLabel or ""
|
||||||
|
self._steamInferredButtonLabels[hash(obj)] = inferredLabel
|
||||||
|
if inferredLabel:
|
||||||
|
tokens = ["STEAM: Inferred label for", obj, ":", inferredLabel]
|
||||||
|
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
||||||
|
|
||||||
|
return inferredLabel
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _getSteamButtonLabelFromClass(className: str) -> str:
|
||||||
|
if "AddFriendButton" in className:
|
||||||
|
return "Add Friend"
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def _getSteamNearbyButtonLabel(self, obj) -> str:
|
||||||
|
parent = AXObject.get_parent(obj)
|
||||||
|
if parent is None:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
siblingLabel = self._getSteamLabelFromChildren(parent, ignore=obj)
|
||||||
|
if siblingLabel:
|
||||||
|
return siblingLabel
|
||||||
|
|
||||||
|
parentLabel = self._getSteamReadableText(parent)
|
||||||
|
if self._isUsefulSteamLabel(parentLabel):
|
||||||
|
return parentLabel
|
||||||
|
|
||||||
|
grandParent = AXObject.get_parent(parent)
|
||||||
|
if grandParent is None:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
return self._getSteamLabelFromChildren(grandParent, ignore=parent)
|
||||||
|
|
||||||
|
def _getSteamLabelFromChildren(self, obj, ignore=None) -> str:
|
||||||
|
for child in AXObject.iter_children(obj):
|
||||||
|
if child == ignore:
|
||||||
|
continue
|
||||||
|
if AXUtilities.is_button(child) or AXUtilities.is_push_button(child):
|
||||||
|
continue
|
||||||
|
|
||||||
|
label = self._getSteamReadableText(child)
|
||||||
|
if self._isUsefulSteamLabel(label):
|
||||||
|
return label
|
||||||
|
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def _getSteamReadableText(self, obj) -> str:
|
||||||
|
if obj is None:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
name = self._normalizeSteamLabelText(AXObject.get_name(obj) or "")
|
||||||
|
if self._isUsefulSteamLabel(name):
|
||||||
|
return name
|
||||||
|
|
||||||
|
if not AXObject.supports_text(obj):
|
||||||
|
return ""
|
||||||
|
|
||||||
|
text = AXText.get_all_text(obj) or ""
|
||||||
|
text = text.replace(self.EMBEDDED_OBJECT_CHARACTER, " ")
|
||||||
|
text = self._normalizeSteamLabelText(text)
|
||||||
|
if self._isUsefulSteamLabel(text):
|
||||||
|
return text
|
||||||
|
|
||||||
|
return ""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _normalizeSteamLabelText(text: str) -> str:
|
||||||
|
return " ".join(text.split())
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _isUsefulSteamLabel(text: Optional[str]) -> bool:
|
||||||
|
if not text:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return not text.isdigit() and text.casefold() != "unlabeled image"
|
||||||
@@ -5093,7 +5093,9 @@ class Utilities(script_utilities.Utilities):
|
|||||||
|
|
||||||
obj, offset = None, -1
|
obj, offset = None, -1
|
||||||
notify = True
|
notify = True
|
||||||
lastWasUp = input_event_manager.get_manager().last_event_was_up()
|
manager = input_event_manager.get_manager()
|
||||||
|
lastWasUp = manager.last_event_was_up()
|
||||||
|
lastWasDown = manager.last_event_was_down()
|
||||||
childCount = AXObject.get_child_count(event.source)
|
childCount = AXObject.get_child_count(event.source)
|
||||||
if lastWasUp:
|
if lastWasUp:
|
||||||
if event.detail1 >= childCount:
|
if event.detail1 >= childCount:
|
||||||
@@ -5112,7 +5114,7 @@ class Utilities(script_utilities.Utilities):
|
|||||||
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
||||||
obj, offset = self.previousContext(prevObj, -1)
|
obj, offset = self.previousContext(prevObj, -1)
|
||||||
|
|
||||||
elif keyString == "Down":
|
elif lastWasDown:
|
||||||
if event.detail1 == 0:
|
if event.detail1 == 0:
|
||||||
msg = "WEB: First child removed. Getting new location from start of parent."
|
msg = "WEB: First child removed. Getting new location from start of parent."
|
||||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
import gi
|
||||||
|
|
||||||
|
gi.require_version("Gdk", "3.0")
|
||||||
|
gi.require_version("Gtk", "3.0")
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
||||||
|
|
||||||
|
soundGeneratorModule = sys.modules.get("cthulhu.sound_generator")
|
||||||
|
if soundGeneratorModule is not None and not hasattr(soundGeneratorModule, "SoundGenerator"):
|
||||||
|
class _StubSoundGenerator:
|
||||||
|
pass
|
||||||
|
|
||||||
|
soundGeneratorModule.SoundGenerator = _StubSoundGenerator
|
||||||
|
|
||||||
|
from gi.repository import Gdk
|
||||||
|
|
||||||
|
from cthulhu import input_event
|
||||||
|
|
||||||
|
|
||||||
|
class KeyboardEventConsumptionTests(unittest.TestCase):
|
||||||
|
def test_script_consumed_key_without_handler_is_treated_as_consumed(self):
|
||||||
|
testScript = mock.Mock()
|
||||||
|
testScript.app = None
|
||||||
|
testScript.keyBindings.getInputHandler.return_value = None
|
||||||
|
testScript.shouldConsumeKeyboardEvent.return_value = True
|
||||||
|
testScript.learnModePresenter.is_active.return_value = False
|
||||||
|
testScript.presentKeyboardEvent.return_value = False
|
||||||
|
|
||||||
|
keyboardEvent = input_event.KeyboardEvent(
|
||||||
|
True,
|
||||||
|
36,
|
||||||
|
Gdk.KEY_Return,
|
||||||
|
0,
|
||||||
|
"Return",
|
||||||
|
)
|
||||||
|
keyboardEvent.set_script(testScript)
|
||||||
|
keyboardEvent.set_object(None)
|
||||||
|
keyboardEvent.set_window(None)
|
||||||
|
|
||||||
|
with (
|
||||||
|
mock.patch("cthulhu.input_event.cthulhu_state.capturingKeys", False),
|
||||||
|
mock.patch("cthulhu.input_event.cthulhu_state.bypassNextCommand", False),
|
||||||
|
):
|
||||||
|
keyboardEvent._finalize_initialization()
|
||||||
|
self.assertTrue(keyboardEvent._should_consume)
|
||||||
|
self.assertEqual(
|
||||||
|
keyboardEvent._consume_reason,
|
||||||
|
"Script consumed without handler",
|
||||||
|
)
|
||||||
|
|
||||||
|
didConsume, resultReason = keyboardEvent._process()
|
||||||
|
|
||||||
|
self.assertTrue(didConsume)
|
||||||
|
self.assertEqual(resultReason, "Consumed during shouldConsume")
|
||||||
|
testScript.presentationInterrupt.assert_called_once()
|
||||||
|
testScript.presentKeyboardEvent.assert_called_once_with(keyboardEvent)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -70,13 +70,13 @@ class SteamNotificationQueueTests(unittest.TestCase):
|
|||||||
testScript._presentSteamNotificationTextNow.assert_not_called()
|
testScript._presentSteamNotificationTextNow.assert_not_called()
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
testScript._steamPendingNotification["text"],
|
testScript._steamPendingNotification["text"],
|
||||||
"username Playing: Game Title",
|
"username Playing: Borderlands 2",
|
||||||
)
|
)
|
||||||
|
|
||||||
testScript._flushSteamPendingNotification(fromTimer=True)
|
testScript._flushSteamPendingNotification(fromTimer=True)
|
||||||
|
|
||||||
testScript._presentSteamNotificationTextNow.assert_called_once_with(
|
testScript._presentSteamNotificationTextNow.assert_called_once_with(
|
||||||
"username Playing: Game Title",
|
"username Playing: Borderlands 2",
|
||||||
notification,
|
notification,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,161 @@
|
|||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
import gi
|
||||||
|
|
||||||
|
gi.require_version("Gdk", "3.0")
|
||||||
|
gi.require_version("Gtk", "3.0")
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
||||||
|
|
||||||
|
soundGeneratorModule = sys.modules.get("cthulhu.sound_generator")
|
||||||
|
if soundGeneratorModule is not None and not hasattr(soundGeneratorModule, "SoundGenerator"):
|
||||||
|
class _StubSoundGenerator:
|
||||||
|
pass
|
||||||
|
|
||||||
|
soundGeneratorModule.SoundGenerator = _StubSoundGenerator
|
||||||
|
|
||||||
|
from cthulhu.scripts.apps.steamwebhelper import script as steam_script
|
||||||
|
from cthulhu.scripts.apps.steamwebhelper import script_utilities as steam_script_utilities
|
||||||
|
|
||||||
|
|
||||||
|
class SteamSelectionChangedTests(unittest.TestCase):
|
||||||
|
def test_selection_changed_tolerates_scalar_any_data(self):
|
||||||
|
testScript = steam_script.Script.__new__(steam_script.Script)
|
||||||
|
source = object()
|
||||||
|
event = mock.Mock(source=source, any_data=0)
|
||||||
|
chromiumCalls = []
|
||||||
|
|
||||||
|
def displayedText(obj):
|
||||||
|
if obj is source:
|
||||||
|
return ""
|
||||||
|
raise TypeError("argument self: Expected Atspi.Accessible, but got int")
|
||||||
|
|
||||||
|
def get_name(obj):
|
||||||
|
if obj is source:
|
||||||
|
return "Notifications"
|
||||||
|
raise TypeError("argument self: Expected Atspi.Accessible, but got int")
|
||||||
|
|
||||||
|
testScript.utilities = mock.Mock()
|
||||||
|
testScript.utilities.displayedText.side_effect = displayedText
|
||||||
|
|
||||||
|
def chromiumOnSelectionChanged(self, selectionEvent):
|
||||||
|
chromiumCalls.append((self, selectionEvent))
|
||||||
|
return True
|
||||||
|
|
||||||
|
with (
|
||||||
|
mock.patch.object(steam_script.AXObject, "get_name", side_effect=get_name),
|
||||||
|
mock.patch.object(steam_script.AXObject, "get_description", return_value=""),
|
||||||
|
mock.patch.object(steam_script.AXObject, "get_role_name", return_value="page tab list"),
|
||||||
|
mock.patch.object(steam_script.AXObject, "get_path", return_value=[1, 2, 3]),
|
||||||
|
mock.patch.object(
|
||||||
|
steam_script.Chromium.Script,
|
||||||
|
"onSelectionChanged",
|
||||||
|
new=chromiumOnSelectionChanged,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
self.assertTrue(testScript.onSelectionChanged(event))
|
||||||
|
|
||||||
|
self.assertEqual(chromiumCalls, [(testScript, event)])
|
||||||
|
|
||||||
|
|
||||||
|
class SteamReturnActivationTests(unittest.TestCase):
|
||||||
|
def test_return_activates_focused_steam_button(self):
|
||||||
|
testScript = steam_script.Script.__new__(steam_script.Script)
|
||||||
|
button = object()
|
||||||
|
keyboardEvent = mock.Mock(event_string="Return", modifiers=0)
|
||||||
|
keyboardEvent.is_pressed_key.return_value = True
|
||||||
|
|
||||||
|
testScript.utilities = mock.Mock()
|
||||||
|
testScript.utilities.inDocumentContent.return_value = True
|
||||||
|
testScript.inFocusMode = mock.Mock(return_value=True)
|
||||||
|
testScript.presentMessage = mock.Mock()
|
||||||
|
testScript._presentDelayedMessage = mock.Mock()
|
||||||
|
testScript._restoreFocusAfterClick = mock.Mock()
|
||||||
|
|
||||||
|
def has_action(obj, action_name):
|
||||||
|
return obj is button and action_name == "press"
|
||||||
|
|
||||||
|
with (
|
||||||
|
mock.patch.object(steam_script.cthulhu_state, "locusOfFocus", button),
|
||||||
|
mock.patch.object(steam_script.AXUtilities, "is_entry", return_value=False),
|
||||||
|
mock.patch.object(steam_script.AXUtilities, "is_text", return_value=False),
|
||||||
|
mock.patch.object(steam_script.AXUtilities, "is_password_text", return_value=False),
|
||||||
|
mock.patch.object(steam_script.AXUtilities, "is_combo_box", return_value=False),
|
||||||
|
mock.patch.object(steam_script.AXUtilities, "is_button", return_value=True),
|
||||||
|
mock.patch.object(steam_script.AXUtilities, "is_push_button", return_value=False),
|
||||||
|
mock.patch.object(steam_script.AXUtilities, "is_link", return_value=False),
|
||||||
|
mock.patch.object(steam_script.AXObject, "has_action", side_effect=has_action),
|
||||||
|
mock.patch.object(steam_script.Script, "_performClickableAction", return_value=True) as performAction,
|
||||||
|
):
|
||||||
|
self.assertTrue(testScript.shouldConsumeKeyboardEvent(keyboardEvent, None))
|
||||||
|
|
||||||
|
performAction.assert_called_once_with(button)
|
||||||
|
|
||||||
|
|
||||||
|
class SteamLabelRecoveryTests(unittest.TestCase):
|
||||||
|
def test_displayed_label_recovers_friends_list_tab_text_from_parent_context(self):
|
||||||
|
testScript = mock.Mock(generatorCache={})
|
||||||
|
utilities = steam_script_utilities.Utilities(testScript)
|
||||||
|
button = object()
|
||||||
|
parent = object()
|
||||||
|
textSibling = object()
|
||||||
|
|
||||||
|
utilities.inDocumentContent = mock.Mock(return_value=True)
|
||||||
|
|
||||||
|
def get_attribute(obj, name):
|
||||||
|
if obj is button and name == "class":
|
||||||
|
return "FriendsListTab Active Panel Focusable gpfocus"
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_name(obj):
|
||||||
|
if obj is textSibling:
|
||||||
|
return "Friends"
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def iter_children(obj, pred=None):
|
||||||
|
children = [textSibling, button]
|
||||||
|
if obj is not parent:
|
||||||
|
children = []
|
||||||
|
if pred is not None:
|
||||||
|
children = [child for child in children if pred(child)]
|
||||||
|
return iter(children)
|
||||||
|
|
||||||
|
with (
|
||||||
|
mock.patch.object(steam_script_utilities.ChromiumUtilities, "displayedLabel", return_value=""),
|
||||||
|
mock.patch.object(steam_script_utilities.AXObject, "get_parent", side_effect=lambda obj: parent if obj is button else None),
|
||||||
|
mock.patch.object(steam_script_utilities.AXObject, "get_attribute", side_effect=get_attribute),
|
||||||
|
mock.patch.object(steam_script_utilities.AXObject, "get_name", side_effect=get_name),
|
||||||
|
mock.patch.object(steam_script_utilities.AXObject, "supports_text", return_value=False),
|
||||||
|
mock.patch.object(steam_script_utilities.AXObject, "iter_children", side_effect=iter_children),
|
||||||
|
mock.patch.object(steam_script_utilities.AXUtilities, "is_button", side_effect=lambda obj: obj is button),
|
||||||
|
mock.patch.object(steam_script_utilities.AXUtilities, "is_push_button", return_value=False),
|
||||||
|
):
|
||||||
|
self.assertEqual(utilities.displayedLabel(button), "Friends")
|
||||||
|
|
||||||
|
def test_displayed_label_maps_add_friend_button_class_to_fallback_name(self):
|
||||||
|
testScript = mock.Mock(generatorCache={})
|
||||||
|
utilities = steam_script_utilities.Utilities(testScript)
|
||||||
|
button = object()
|
||||||
|
|
||||||
|
utilities.inDocumentContent = mock.Mock(return_value=True)
|
||||||
|
|
||||||
|
def get_attribute(obj, name):
|
||||||
|
if obj is button and name == "class":
|
||||||
|
return "friendListButton AddFriendButton Panel Focusable"
|
||||||
|
return None
|
||||||
|
|
||||||
|
with (
|
||||||
|
mock.patch.object(steam_script_utilities.ChromiumUtilities, "displayedLabel", return_value=""),
|
||||||
|
mock.patch.object(steam_script_utilities.AXObject, "get_attribute", side_effect=get_attribute),
|
||||||
|
mock.patch.object(steam_script_utilities.AXObject, "get_name", return_value=""),
|
||||||
|
mock.patch.object(steam_script_utilities.AXUtilities, "is_button", side_effect=lambda obj: obj is button),
|
||||||
|
mock.patch.object(steam_script_utilities.AXUtilities, "is_push_button", return_value=False),
|
||||||
|
):
|
||||||
|
self.assertEqual(utilities.displayedLabel(button), "Add Friend")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -108,5 +108,45 @@ class WebHiddenPopupTests(unittest.TestCase):
|
|||||||
self.assertFalse(utilities._canHaveCaretContext(hiddenObject))
|
self.assertFalse(utilities._canHaveCaretContext(hiddenObject))
|
||||||
|
|
||||||
|
|
||||||
|
class WebRemovedChildRegressionTests(unittest.TestCase):
|
||||||
|
def test_removed_child_recovery_does_not_crash_when_last_key_is_not_up_or_down(self):
|
||||||
|
utilities = script_utilities.Utilities.__new__(script_utilities.Utilities)
|
||||||
|
removedChild = object()
|
||||||
|
locusOfFocus = object()
|
||||||
|
source = object()
|
||||||
|
recoveredObject = object()
|
||||||
|
event = mock.Mock(any_data=removedChild, source=source, detail1=0)
|
||||||
|
|
||||||
|
utilities._script = mock.Mock(pointOfReference={"names": {}})
|
||||||
|
utilities._handleEventForRemovedListBoxChild = mock.Mock(return_value=False)
|
||||||
|
utilities.isSameObject = mock.Mock(return_value=False)
|
||||||
|
utilities.searchForCaretContext = mock.Mock(return_value=(recoveredObject, 0))
|
||||||
|
utilities.setCaretContext = mock.Mock()
|
||||||
|
|
||||||
|
manager = mock.Mock()
|
||||||
|
manager.last_event_was_up.return_value = False
|
||||||
|
manager.last_event_was_down.return_value = False
|
||||||
|
|
||||||
|
def find_ancestor(obj, predicate):
|
||||||
|
if obj is locusOfFocus and predicate(removedChild):
|
||||||
|
return removedChild
|
||||||
|
return None
|
||||||
|
|
||||||
|
with (
|
||||||
|
mock.patch.object(script_utilities.cthulhu_state, "locusOfFocus", locusOfFocus),
|
||||||
|
mock.patch.object(script_utilities.input_event_manager, "get_manager", return_value=manager),
|
||||||
|
mock.patch.object(script_utilities.AXObject, "find_ancestor", side_effect=find_ancestor),
|
||||||
|
mock.patch.object(script_utilities.AXObject, "get_child_count", return_value=0),
|
||||||
|
mock.patch.object(script_utilities.AXObject, "clear_cache"),
|
||||||
|
mock.patch.object(script_utilities.AXObject, "is_dead", return_value=False),
|
||||||
|
mock.patch.object(script_utilities.AXUtilities, "get_focused_object", return_value=None),
|
||||||
|
mock.patch.object(script_utilities.cthulhu, "setLocusOfFocus") as setLocusOfFocus,
|
||||||
|
):
|
||||||
|
self.assertTrue(utilities.handleEventForRemovedChild(event))
|
||||||
|
|
||||||
|
setLocusOfFocus.assert_called_once_with(event, recoveredObject, False)
|
||||||
|
utilities.setCaretContext.assert_called_once_with(recoveredObject, 0)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user