Initial work on making Steam overlay accessible.

This commit is contained in:
Storm Dragon
2026-07-10 11:43:35 -04:00
parent 278a914630
commit 0858ec441a
4 changed files with 1335 additions and 4 deletions
+123 -2
View File
@@ -328,6 +328,118 @@ class EventManager:
def _isSteamNotificationEvent(self, event: Atspi.Event) -> bool: def _isSteamNotificationEvent(self, event: Atspi.Event) -> bool:
return self._isLiveOrNotificationEvent(event) return self._isLiveOrNotificationEvent(event)
def _isSteamOverlayNavigationEvent(self, event: Atspi.Event, app: Atspi.Accessible) -> bool:
if not self._isSteamApp(app):
return False
if not event.type.startswith((
"object:property-change:accessible-name",
"object:active-descendant-changed",
)):
return False
if not self._activeX11WindowIsSteamSelectionContext():
return False
return self._eventHasSteamQuickAccessContext(event)
def _eventHasSteamQuickAccessContext(self, event: Atspi.Event) -> bool:
for obj in (event.source, getattr(event, "any_data", None)):
if obj is None or isinstance(obj, (str, bytes, int, float, bool)):
continue
try:
if self._hasSteamQuickAccessMarker(obj):
return True
except Exception:
continue
return False
@staticmethod
def _hasSteamQuickAccessMarker(obj: Any) -> bool:
def hasMarker(candidate: Any) -> bool:
if candidate is None:
return False
name = AXObject.get_name(candidate) or ""
if name.startswith("QuickAccess_uid"):
return True
accessibleId = AXObject.get_attribute(candidate, "id") or ""
if accessibleId.startswith("quickaccess_"):
return True
className = AXObject.get_attribute(candidate, "class") or ""
return "quickaccess" in className.casefold()
return AXObject.find_ancestor_inclusive(obj, hasMarker) is not None
def _activeX11WindowIsSteamSelectionContext(self) -> bool:
window = input_event_manager.get_manager()._get_active_x11_window()
return self._x11WindowHasSteamGameClass(window) or self._x11WindowIsSteamBigPicture(window)
@classmethod
def _x11WindowHasSteamGameClass(cls, window: Any) -> bool:
if window is None:
return False
identifiers = [
cls._safeSteamWindowCall(window, "get_class_group_name"),
cls._safeSteamWindowCall(window, "get_class_instance_name"),
]
classGroup = cls._safeSteamWindowCall(window, "get_class_group")
if classGroup is not None:
identifiers.extend([
cls._safeSteamWindowCall(classGroup, "get_name"),
cls._safeSteamWindowCall(classGroup, "get_res_class"),
])
return any(cls._isSteamGameWindowIdentifier(identifier) for identifier in identifiers)
@classmethod
def _x11WindowIsSteamBigPicture(cls, window: Any) -> bool:
if window is None:
return False
name = cls._safeSteamWindowCall(window, "get_name")
if not isinstance(name, str) or name.strip() != "Steam Big Picture Mode":
return False
identifiers = [
cls._safeSteamWindowCall(window, "get_class_group_name"),
cls._safeSteamWindowCall(window, "get_class_instance_name"),
]
classGroup = cls._safeSteamWindowCall(window, "get_class_group")
if classGroup is not None:
identifiers.extend([
cls._safeSteamWindowCall(classGroup, "get_name"),
cls._safeSteamWindowCall(classGroup, "get_res_class"),
])
normalized = {
identifier.strip().casefold()
for identifier in identifiers
if isinstance(identifier, str)
}
return bool(normalized.intersection({"steam", "steamwebhelper"}))
@staticmethod
def _safeSteamWindowCall(window: Any, attrName: str):
attr = getattr(window, attrName, None)
if not callable(attr):
return None
try:
return attr()
except Exception:
return None
@staticmethod
def _isSteamGameWindowIdentifier(identifier: Any) -> bool:
if not isinstance(identifier, str):
return False
return identifier.strip().casefold().startswith("steam_app_")
def _isLiveOrNotificationObject(self, obj: Any) -> bool: def _isLiveOrNotificationObject(self, obj: Any) -> bool:
if obj is None or isinstance(obj, (str, bytes, int, float, bool)): if obj is None or isinstance(obj, (str, bytes, int, float, bool)):
return False return False
@@ -559,18 +671,27 @@ class EventManager:
if self._inDeluge() and self._ignoreDuringDeluge(event): if self._inDeluge() and self._ignoreDuringDeluge(event):
return _ignore_with_reason("deluge", "event type during deluge") return _ignore_with_reason("deluge", "event type during deluge")
isSteamOverlayNavigationEvent = self._isSteamOverlayNavigationEvent(event, app)
script = cthulhu_state.activeScript script = cthulhu_state.activeScript
if event.type.startswith('object:children-changed') \ if event.type.startswith('object:children-changed') \
or event.type.startswith('object:state-changed:sensitive'): or event.type.startswith('object:state-changed:sensitive'):
if not script: if not script:
return _ignore_with_reason("no-active-script", "no active script") if isSteamOverlayNavigationEvent:
if script.app != app: _log_allow("steam-overlay-navigation", "no active script")
else:
return _ignore_with_reason("no-active-script", "no active script")
elif script.app != app:
# Allow Steam notifications from inactive apps. # Allow Steam notifications from inactive apps.
if self._isSteamApp(app) and self._isSteamNotificationEvent(event): if self._isSteamApp(app) and self._isSteamNotificationEvent(event):
_log_allow("steam-notification", "inactive app notification") _log_allow("steam-notification", "inactive app notification")
elif isSteamOverlayNavigationEvent:
_log_allow("steam-overlay-navigation", "inactive app overlay navigation")
else: else:
return _ignore_with_reason("inactive-app", "event not from active app") return _ignore_with_reason("inactive-app", "event not from active app")
if isSteamOverlayNavigationEvent:
return _allow_with_reason("steam-overlay-navigation", "Steam overlay navigation event")
relevance = self._classifyRelevance(event, app) relevance = self._classifyRelevance(event, app)
if relevance == self.RELEVANCE_DROP: if relevance == self.RELEVANCE_DROP:
return _ignore_with_reason("event-relevance-drop", "event is background churn") return _ignore_with_reason("event-relevance-drop", "event is background churn")
@@ -29,15 +29,19 @@ __license__ = "LGPL"
import time import time
import re import re
from typing import Any
from gi.repository import GLib from gi.repository import GLib
from cthulhu import debug from cthulhu import debug
from cthulhu import input_event_manager
from cthulhu import settings from cthulhu import settings
from cthulhu import cthulhu_state from cthulhu import cthulhu_state
from cthulhu import settings_manager from cthulhu import settings_manager
from cthulhu import structural_navigation from cthulhu import structural_navigation
from cthulhu.ax_object import AXObject from cthulhu.ax_object import AXObject
from cthulhu.ax_selection import AXSelection
from cthulhu.ax_text import AXText
from cthulhu.ax_utilities import AXUtilities 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
@@ -84,6 +88,10 @@ class Script(Chromium.Script):
self._lastSteamNotification = ("", 0.0) self._lastSteamNotification = ("", 0.0)
self._steamPendingNotification = None self._steamPendingNotification = None
self._steamNotificationDelayMs = 500 self._steamNotificationDelayMs = 500
self._lastSteamOverlaySelection = ("", 0.0)
self._steamActiveQuickAccessTab = ""
self._steamFriendsPanelActivationTarget = (None, "", 0.0)
self._steamFriendsPanelActivationTargetMaxAge = 10.0
self._steamRelativeTimePattern = re.compile( self._steamRelativeTimePattern = re.compile(
r"^(?:just now|now|\d+\s+(?:second|minute|hour|day|week|month|year)s?\s+ago)$", r"^(?:just now|now|\d+\s+(?:second|minute|hour|day|week|month|year)s?\s+ago)$",
re.IGNORECASE re.IGNORECASE
@@ -118,6 +126,9 @@ class Script(Chromium.Script):
self._presentSteamNotification(event.any_data) self._presentSteamNotification(event.any_data)
return return
if self._presentSteamFriendsPanelNavigation(event):
return True
if self._handleSteamVirtualizedListMutation(event): if self._handleSteamVirtualizedListMutation(event):
return True return True
@@ -156,12 +167,24 @@ class Script(Chromium.Script):
return super().onFocusedChanged(event) return super().onFocusedChanged(event)
def onNameChanged(self, event):
"""Callback for object:property-change:accessible-name events."""
self._logSteamNavigationEvent("name-changed", event)
if self._presentSteamFriendsPanelNavigation(event):
return True
return super().onNameChanged(event)
def onTextInserted(self, event): def onTextInserted(self, event):
"""Callback for object:text-changed:insert accessibility events.""" """Callback for object:text-changed:insert accessibility events."""
if self._presentSteamLiveRegionText(event): if self._presentSteamLiveRegionText(event):
return return
if self._presentSteamFriendsPanelNavigation(event):
return True
super().onTextInserted(event) super().onTextInserted(event)
def onCaretMoved(self, event): def onCaretMoved(self, event):
@@ -178,8 +201,18 @@ class Script(Chromium.Script):
"""Callback for object:selection-changed accessibility events.""" """Callback for object:selection-changed accessibility events."""
self._logSteamNavigationEvent("selection-changed", event) self._logSteamNavigationEvent("selection-changed", event)
if self._presentSteamOverlaySelection(event):
return True
return super().onSelectionChanged(event) return super().onSelectionChanged(event)
def onSelectedChanged(self, event):
"""Callback for object:state-changed:selected accessibility events."""
self._logSteamNavigationEvent("selected-changed", event)
if self._presentSteamOverlaySelection(event):
return True
return super().onSelectedChanged(event)
def onActiveDescendantChanged(self, event): def onActiveDescendantChanged(self, event):
"""Callback for object:active-descendant-changed accessibility events.""" """Callback for object:active-descendant-changed accessibility events."""
@@ -203,6 +236,445 @@ class Script(Chromium.Script):
debug.printMessage(debug.LEVEL_INFO, msg, True) debug.printMessage(debug.LEVEL_INFO, msg, True)
return True return True
def _presentSteamOverlaySelection(self, event) -> bool:
if not event:
return False
eventType = getattr(event, "type", "")
if not isinstance(eventType, str) or not (
eventType.startswith("object:selection-changed")
or eventType.startswith("object:state-changed:selected")
):
return False
if not self._activeX11WindowIsSteamSelectionContext():
return False
if not self._steamOverlaySelectionWasUserInitiated():
return False
obj = self._getSteamOverlaySelectionObject(event)
if not obj or not self._isSteamOverlaySelectionObject(obj):
return False
text = self._getSteamOverlaySelectionText(obj)
if not text:
return False
if AXUtilities.is_page_tab(obj):
self._steamActiveQuickAccessTab = text
self._clearSteamFriendsPanelActivationTarget()
if self._isDuplicateSteamOverlaySelection(text):
msg = f"STEAM: Suppressing duplicate overlay selection: {text}"
debug.printMessage(debug.LEVEL_INFO, msg, True)
return True
self.presentMessage(text, force=True, interrupt=True)
msg = f"STEAM: Presented overlay selection: {text}"
debug.printMessage(debug.LEVEL_INFO, msg, True)
return True
def _getSteamOverlaySelectionObject(self, event):
if event.type.startswith("object:state-changed:selected"):
if not event.detail1:
return None
return event.source
if not event.type.startswith("object:selection-changed"):
return None
for child in AXSelection.get_selected_children(event.source):
if child:
return child
anyData = event.any_data
if anyData is not None and not isinstance(anyData, (bool, int, float, str)):
return anyData
return event.source
def _isSteamOverlaySelectionObject(self, obj) -> bool:
if not obj:
return False
try:
if not self.utilities.inDocumentContent(obj):
return False
except Exception:
return False
if not self._hasSteamOverlaySelectionContext(obj):
return False
if not (
AXUtilities.is_page_tab(obj)
or AXUtilities.is_list_item(obj)
or AXUtilities.is_menu_item(obj)
or AXUtilities.is_button(obj)
or AXUtilities.is_push_button(obj)
):
return False
return bool(self._getSteamOverlaySelectionText(obj))
def _getSteamOverlaySelectionText(self, obj) -> str:
text = self.utilities.displayedText(obj) or AXObject.get_name(obj) or ""
text = self._normalizeSteamNotificationText(text)
if text:
return text
return self._getSteamQuickAccessTabLabel(obj)
def _getSteamQuickAccessTabLabel(self, obj) -> str:
if not AXUtilities.is_page_tab(obj):
return ""
if not self._hasSteamQuickAccessMarker(obj):
return ""
tabId = AXObject.get_attribute(obj, "id") or ""
quickAccessTabLabels = {
"quickaccess_tab_3": "Friends",
}
label = quickAccessTabLabels.get(tabId, "")
if label:
msg = f"STEAM: Inferred QuickAccess tab label: {label}"
debug.printMessage(debug.LEVEL_INFO, msg, True)
return label
def _presentSteamFriendsPanelNavigation(self, event) -> bool:
if not self._steamFriendsPanelNavigationIsExpected(event):
return False
obj = self._getSteamFriendsPanelNavigationObject(event)
if not obj or not self._isSteamFriendsPanelNavigationObject(obj):
return False
text = self._getSteamFriendsPanelNavigationText(obj)
if not text:
return False
self._rememberSteamFriendsPanelActivationTarget(obj, text)
if self._isDuplicateSteamOverlaySelection(text):
msg = f"STEAM: Suppressing duplicate Friends panel item: {text}"
debug.printMessage(debug.LEVEL_INFO, msg, True)
return True
self.presentMessage(text, force=True, interrupt=True)
msg = f"STEAM: Presented Friends panel item: {text}"
debug.printMessage(debug.LEVEL_INFO, msg, True)
return True
def _steamFriendsPanelNavigationIsExpected(self, event) -> bool:
if not event:
return False
eventType = getattr(event, "type", "")
if not isinstance(eventType, str) or not eventType.startswith((
"object:property-change:accessible-name",
"object:active-descendant-changed",
)):
return False
if getattr(self, "_steamActiveQuickAccessTab", "") != "Friends":
return False
if not self._activeX11WindowIsSteamSelectionContext():
return False
return self._steamOverlaySelectionWasUserInitiated()
@staticmethod
def _getSteamFriendsPanelNavigationObject(event):
anyData = getattr(event, "any_data", None)
if anyData is not None and not isinstance(anyData, (bool, int, float, str, bytes)):
return anyData
return getattr(event, "source", None)
def _isSteamFriendsPanelNavigationObject(self, obj) -> bool:
try:
if not self._hasSteamQuickAccessMarker(obj):
return False
except Exception:
return False
if AXUtilities.is_page_tab(obj) or AXUtilities.is_page_tab_list(obj):
return False
if self._isIgnoredSteamFriendsPanelObject(obj):
return False
return (
AXUtilities.is_list_item(obj)
or AXUtilities.is_button(obj)
or AXUtilities.is_push_button(obj)
or AXUtilities.is_table_row(obj)
)
def _isIgnoredSteamFriendsPanelObject(self, obj) -> bool:
def isIgnored(candidate) -> bool:
if candidate is None:
return False
className = AXObject.get_attribute(candidate, "class") or ""
accessibleId = AXObject.get_attribute(candidate, "id") or ""
searchable = f"{className} {accessibleId}".casefold()
ignoredMarkers = (
"addfriendbutton",
"chathistorycontainer",
"chatdialogs",
"chatwindow",
"chatentry",
"chatmessage",
"messagelist",
"messageinput",
"textarea",
)
return any(marker in searchable for marker in ignoredMarkers)
try:
return AXObject.find_ancestor_inclusive(obj, isIgnored) is not None
except Exception:
return False
def _getSteamFriendsPanelNavigationText(self, obj) -> str:
text = self._getSteamFriendsPanelReadableText(obj)
if self._isUsefulSteamFriendsPanelText(text):
return text
return ""
def _rememberSteamFriendsPanelActivationTarget(self, obj: Any, text: str) -> None:
self._steamFriendsPanelActivationTarget = (obj, text, time.monotonic())
def _clearSteamFriendsPanelActivationTarget(self) -> None:
self._steamFriendsPanelActivationTarget = (None, "", 0.0)
def _getSteamFriendsPanelActivationTarget(self) -> Any:
if getattr(self, "_steamActiveQuickAccessTab", "") != "Friends":
return None
if not self._activeX11WindowIsSteamSelectionContext():
return None
obj, text, timestamp = getattr(
self,
"_steamFriendsPanelActivationTarget",
(None, "", 0.0),
)
if not obj or not text:
return None
maxAge = getattr(self, "_steamFriendsPanelActivationTargetMaxAge", 10.0)
if time.monotonic() - timestamp > maxAge:
msg = f"STEAM: Clearing stale Friends panel activation target: {text}"
debug.printMessage(debug.LEVEL_INFO, msg, True)
self._clearSteamFriendsPanelActivationTarget()
return None
try:
if AXObject.is_dead(obj):
self._clearSteamFriendsPanelActivationTarget()
return None
except Exception:
self._clearSteamFriendsPanelActivationTarget()
return None
if not self._isSteamFriendsPanelNavigationObject(obj):
self._clearSteamFriendsPanelActivationTarget()
return None
return obj
def _getSteamFriendsPanelReadableText(self, obj) -> str:
text = ""
try:
text = self.utilities.displayedText(obj) or AXObject.get_name(obj) or ""
except Exception:
try:
text = AXObject.get_name(obj) or ""
except Exception:
text = ""
text = self._normalizeSteamFriendsPanelText(text)
if text:
return text
try:
if AXObject.supports_text(obj):
text = AXText.get_all_text(obj) or ""
except Exception:
text = ""
return self._normalizeSteamFriendsPanelText(text)
def _normalizeSteamFriendsPanelText(self, text: str) -> str:
if not text:
return ""
text = text.replace(self.EMBEDDED_OBJECT_CHARACTER, " ")
text = text.replace("[OBJ]", " ")
return " ".join(text.split())
@staticmethod
def _isUsefulSteamFriendsPanelText(text: str) -> bool:
if not text:
return False
textLower = text.casefold()
if textLower in {"friends", "add friend", "unlabeled image"}:
return False
ignoredFragments = (
"loading older messages",
"hold to send a quick message",
"is typing a message",
)
if any(fragment in textLower for fragment in ignoredFragments):
return False
return not text.isdigit()
def _hasSteamQuickAccessMarker(self, obj) -> bool:
def hasMarker(candidate) -> bool:
if candidate is None:
return False
name = AXObject.get_name(candidate) or ""
if name.startswith("QuickAccess_uid"):
return True
accessibleId = AXObject.get_attribute(candidate, "id") or ""
if accessibleId.startswith("quickaccess_"):
return True
className = AXObject.get_attribute(candidate, "class") or ""
return "quickaccess" in className.casefold()
return AXObject.find_ancestor_inclusive(obj, hasMarker) is not None
def _hasSteamOverlaySelectionContext(self, obj) -> bool:
if self._hasSteamQuickAccessMarker(obj):
return True
return self._isSteamBigPictureDocumentObject(obj)
def _isSteamBigPictureDocumentObject(self, obj) -> bool:
try:
document = self.utilities.getTopLevelDocumentForObject(obj)
if not document:
document = self.utilities.getDocumentForObject(obj)
except Exception:
return False
if not document:
return False
try:
uri = self.utilities.documentFrameURI(document) or ""
except Exception:
uri = ""
if "steamloopback.host" in uri.casefold():
return True
name = AXObject.get_name(document) or ""
return name.strip() == "Steam Big Picture Mode"
def _steamOverlaySelectionWasUserInitiated(self) -> bool:
event = cthulhu_state.lastNonModifierKeyEvent or cthulhu_state.lastInputEvent
if event is None:
return False
modifiers = getattr(event, "modifiers", 0) or 0
if modifiers:
return False
key = getattr(event, "event_string", None) or getattr(event, "keyval_name", "")
return key in {"Left", "Right", "Up", "Down", "Return", "KP_Enter", "space", " "}
def _activeX11WindowIsSteamGame(self) -> bool:
window = input_event_manager.get_manager()._get_active_x11_window()
return self._x11WindowHasSteamGameClass(window)
def _activeX11WindowIsSteamSelectionContext(self) -> bool:
window = input_event_manager.get_manager()._get_active_x11_window()
return self._x11WindowHasSteamGameClass(window) or self._x11WindowIsSteamBigPicture(window)
@classmethod
def _x11WindowHasSteamGameClass(cls, window) -> bool:
if window is None:
return False
identifiers = [
cls._safeSteamWindowCall(window, "get_class_group_name"),
cls._safeSteamWindowCall(window, "get_class_instance_name"),
]
classGroup = cls._safeSteamWindowCall(window, "get_class_group")
if classGroup is not None:
identifiers.extend([
cls._safeSteamWindowCall(classGroup, "get_name"),
cls._safeSteamWindowCall(classGroup, "get_res_class"),
])
return any(cls._isSteamGameWindowIdentifier(identifier) for identifier in identifiers)
@classmethod
def _x11WindowIsSteamBigPicture(cls, window) -> bool:
if window is None:
return False
name = cls._safeSteamWindowCall(window, "get_name")
if not isinstance(name, str) or name.strip() != "Steam Big Picture Mode":
return False
identifiers = [
cls._safeSteamWindowCall(window, "get_class_group_name"),
cls._safeSteamWindowCall(window, "get_class_instance_name"),
]
classGroup = cls._safeSteamWindowCall(window, "get_class_group")
if classGroup is not None:
identifiers.extend([
cls._safeSteamWindowCall(classGroup, "get_name"),
cls._safeSteamWindowCall(classGroup, "get_res_class"),
])
normalized = {
identifier.strip().casefold()
for identifier in identifiers
if isinstance(identifier, str)
}
return bool(normalized.intersection({"steam", "steamwebhelper"}))
@staticmethod
def _safeSteamWindowCall(window: Any, attrName: str):
attr = getattr(window, attrName, None)
if not callable(attr):
return None
try:
return attr()
except Exception:
return None
@staticmethod
def _isSteamGameWindowIdentifier(identifier) -> bool:
if not isinstance(identifier, str):
return False
return identifier.strip().casefold().startswith("steam_app_")
def _isDuplicateSteamOverlaySelection(self, text: str) -> bool:
lastText, lastTime = getattr(self, "_lastSteamOverlaySelection", ("", 0.0))
now = time.monotonic()
if text == lastText and now - lastTime < 0.75:
return True
self._lastSteamOverlaySelection = (text, now)
return False
def _isSteamLoadingSpinnerCaretEvent(self, event) -> bool: def _isSteamLoadingSpinnerCaretEvent(self, event) -> bool:
if not (event and event.type.startswith("object:text-caret-moved")): if not (event and event.type.startswith("object:text-caret-moved")):
return False return False
@@ -241,8 +713,15 @@ class Script(Chromium.Script):
if getattr(keyboardEvent, "modifiers", 0): if getattr(keyboardEvent, "modifiers", 0):
return False return False
obj = self._getClickableActivationTarget() friendsPanelTarget = self._getSteamFriendsPanelActivationTarget()
if not obj or not self.utilities.inDocumentContent(obj): obj = friendsPanelTarget or self._getClickableActivationTarget()
if not obj:
return False
try:
if not self.utilities.inDocumentContent(obj):
return False
except Exception:
return False return False
if not (AXUtilities.is_button(obj) or AXUtilities.is_push_button(obj)): if not (AXUtilities.is_button(obj) or AXUtilities.is_push_button(obj)):
@@ -255,16 +734,33 @@ class Script(Chromium.Script):
return False return False
if self._performClickableAction(obj): if self._performClickableAction(obj):
self._logSteamFriendsPanelActivation(friendsPanelTarget)
return True return True
from cthulhu import ax_event_synthesizer from cthulhu import ax_event_synthesizer
result = ax_event_synthesizer.AXEventSynthesizer.click_object(obj) result = ax_event_synthesizer.AXEventSynthesizer.click_object(obj)
if result: if result:
self._logSteamFriendsPanelActivation(friendsPanelTarget)
self._restoreFocusAfterClick(obj) self._restoreFocusAfterClick(obj)
return True return True
return False return False
def _logSteamFriendsPanelActivation(self, obj: Any) -> None:
if obj is None:
return
target, text, _timestamp = getattr(
self,
"_steamFriendsPanelActivationTarget",
(None, "", 0.0),
)
if obj is not target or not text:
return
msg = f"STEAM: Activated Friends panel item: {text}"
debug.printMessage(debug.LEVEL_INFO, msg, True)
def _isSteamNotification(self, obj): def _isSteamNotification(self, obj):
"""Detect if object is a Steam notification. """Detect if object is a Steam notification.
@@ -182,6 +182,42 @@ class EventManagerCompositorContextRegressionTests(unittest.TestCase):
self.assertFalse(self.manager._ignore(firstEvent)) self.assertFalse(self.manager._ignore(firstEvent))
self.assertTrue(self.manager._ignore(secondEvent)) self.assertTrue(self.manager._ignore(secondEvent))
def test_steam_quick_access_name_changed_survives_without_active_script(self) -> None:
app = object()
event = FakeEvent("object:property-change:accessible-name", source="quick-access")
cthulhu_state.activeScript = None
with (
mock.patch.object(event_manager.debug, "printMessage"),
mock.patch.object(event_manager.debug, "printTokens"),
mock.patch.object(event_manager.debug, "print_log"),
mock.patch.object(event_manager.AXObject, "get_application", return_value=app),
mock.patch.object(event_manager.AXObject, "get_name", return_value="steamwebhelper"),
):
self.manager._isSteamOverlayNavigationEvent = mock.Mock(return_value=True)
self.assertFalse(self.manager._ignore(event))
def test_steam_overlay_navigation_requires_quick_access_context(self) -> None:
app = object()
event = FakeEvent("object:property-change:accessible-name", source="not-quick-access")
self.manager._isSteamApp = mock.Mock(return_value=True)
self.manager._activeX11WindowIsSteamSelectionContext = mock.Mock(return_value=True)
self.manager._eventHasSteamQuickAccessContext = mock.Mock(return_value=False)
self.assertFalse(self.manager._isSteamOverlayNavigationEvent(event, app))
def test_steam_overlay_navigation_rejects_children_changed_churn(self) -> None:
app = object()
event = FakeEvent("object:children-changed:add", source="quick-access", any_data=object())
self.manager._isSteamApp = mock.Mock(return_value=True)
self.manager._activeX11WindowIsSteamSelectionContext = mock.Mock(return_value=True)
self.manager._eventHasSteamQuickAccessContext = mock.Mock(return_value=True)
self.assertFalse(self.manager._isSteamOverlayNavigationEvent(event, app))
def test_steam_focus_lost_burst_is_ignored_but_focus_gain_is_preserved(self) -> None: def test_steam_focus_lost_burst_is_ignored_but_focus_gain_is_preserved(self) -> None:
app = object() app = object()
cthulhu_state.activeScript = mock.Mock(app=app) cthulhu_state.activeScript = mock.Mock(app=app)
+678
View File
@@ -61,6 +61,409 @@ class SteamSelectionChangedTests(unittest.TestCase):
self.assertEqual(chromiumCalls, [(testScript, event)]) self.assertEqual(chromiumCalls, [(testScript, event)])
class SteamOverlaySelectionPresentationTests(unittest.TestCase):
def test_selected_changed_presents_quick_access_item_over_steam_game(self):
testScript = steam_script.Script.__new__(steam_script.Script)
source = object()
lastEvent = mock.Mock(event_string="Down", modifiers=0)
event = mock.Mock(
type="object:state-changed:selected",
source=source,
detail1=1,
any_data=None,
)
testScript.utilities = mock.Mock()
testScript.utilities.inDocumentContent.return_value = True
testScript.utilities.displayedText.return_value = ""
testScript.presentMessage = mock.Mock()
testScript._lastSteamOverlaySelection = ("", 0.0)
chromiumCalls = []
def chromiumOnSelectedChanged(self, selectedEvent):
chromiumCalls.append((self, selectedEvent))
return False
with (
mock.patch.object(
steam_script.Script,
"_activeX11WindowIsSteamSelectionContext",
return_value=True,
),
mock.patch.object(steam_script.Script, "_hasSteamQuickAccessMarker", return_value=True),
mock.patch.object(steam_script.cthulhu_state, "lastNonModifierKeyEvent", lastEvent),
mock.patch.object(steam_script.cthulhu_state, "lastInputEvent", None),
mock.patch.object(steam_script.AXObject, "get_name", return_value="Quick Settings"),
mock.patch.object(steam_script.AXUtilities, "is_page_tab", return_value=True),
mock.patch.object(steam_script.AXUtilities, "is_list_item", return_value=False),
mock.patch.object(steam_script.AXUtilities, "is_menu_item", return_value=False),
mock.patch.object(steam_script.AXUtilities, "is_button", return_value=False),
mock.patch.object(steam_script.AXUtilities, "is_push_button", return_value=False),
mock.patch.object(
steam_script.Chromium.Script,
"onSelectedChanged",
new=chromiumOnSelectedChanged,
),
):
self.assertTrue(testScript.onSelectedChanged(event))
testScript.presentMessage.assert_called_once_with(
"Quick Settings",
force=True,
interrupt=True,
)
self.assertEqual(chromiumCalls, [])
def test_selected_changed_presents_inferred_quick_access_friends_tab_label(self):
testScript = steam_script.Script.__new__(steam_script.Script)
source = object()
lastEvent = mock.Mock(event_string="Down", modifiers=0)
event = mock.Mock(
type="object:state-changed:selected",
source=source,
detail1=1,
any_data=None,
)
testScript.utilities = mock.Mock()
testScript.utilities.inDocumentContent.return_value = True
testScript.utilities.displayedText.return_value = ""
testScript.presentMessage = mock.Mock()
testScript._lastSteamOverlaySelection = ("", 0.0)
chromiumCalls = []
def chromiumOnSelectedChanged(self, selectedEvent):
chromiumCalls.append((self, selectedEvent))
return False
with (
mock.patch.object(
steam_script.Script,
"_activeX11WindowIsSteamSelectionContext",
return_value=True,
),
mock.patch.object(steam_script.Script, "_hasSteamQuickAccessMarker", return_value=True),
mock.patch.object(steam_script.cthulhu_state, "lastNonModifierKeyEvent", lastEvent),
mock.patch.object(steam_script.cthulhu_state, "lastInputEvent", None),
mock.patch.object(steam_script.AXObject, "get_name", return_value=""),
mock.patch.object(
steam_script.AXObject,
"get_attribute",
side_effect=lambda obj, name: "quickaccess_tab_3" if name == "id" else "",
),
mock.patch.object(steam_script.AXUtilities, "is_page_tab", return_value=True),
mock.patch.object(steam_script.AXUtilities, "is_list_item", return_value=False),
mock.patch.object(steam_script.AXUtilities, "is_menu_item", return_value=False),
mock.patch.object(steam_script.AXUtilities, "is_button", return_value=False),
mock.patch.object(steam_script.AXUtilities, "is_push_button", return_value=False),
mock.patch.object(
steam_script.Chromium.Script,
"onSelectedChanged",
new=chromiumOnSelectedChanged,
),
):
self.assertTrue(testScript.onSelectedChanged(event))
testScript.presentMessage.assert_called_once_with(
"Friends",
force=True,
interrupt=True,
)
self.assertEqual(chromiumCalls, [])
self.assertEqual(testScript._steamActiveQuickAccessTab, "Friends")
def test_selected_changed_defers_unknown_unnamed_quick_access_tab(self):
testScript = steam_script.Script.__new__(steam_script.Script)
source = object()
lastEvent = mock.Mock(event_string="Down", modifiers=0)
event = mock.Mock(
type="object:state-changed:selected",
source=source,
detail1=1,
any_data=None,
)
testScript.utilities = mock.Mock()
testScript.utilities.inDocumentContent.return_value = True
testScript.utilities.displayedText.return_value = ""
testScript.presentMessage = mock.Mock()
chromiumCalls = []
def chromiumOnSelectedChanged(self, selectedEvent):
chromiumCalls.append((self, selectedEvent))
return "handled"
with (
mock.patch.object(
steam_script.Script,
"_activeX11WindowIsSteamSelectionContext",
return_value=True,
),
mock.patch.object(steam_script.Script, "_hasSteamQuickAccessMarker", return_value=True),
mock.patch.object(steam_script.cthulhu_state, "lastNonModifierKeyEvent", lastEvent),
mock.patch.object(steam_script.cthulhu_state, "lastInputEvent", None),
mock.patch.object(steam_script.AXObject, "get_name", return_value=""),
mock.patch.object(
steam_script.AXObject,
"get_attribute",
side_effect=lambda obj, name: "quickaccess_tab_unknown" if name == "id" else "",
),
mock.patch.object(steam_script.AXUtilities, "is_page_tab", return_value=True),
mock.patch.object(steam_script.AXUtilities, "is_list_item", return_value=False),
mock.patch.object(steam_script.AXUtilities, "is_menu_item", return_value=False),
mock.patch.object(steam_script.AXUtilities, "is_button", return_value=False),
mock.patch.object(steam_script.AXUtilities, "is_push_button", return_value=False),
mock.patch.object(
steam_script.Chromium.Script,
"onSelectedChanged",
new=chromiumOnSelectedChanged,
),
):
self.assertEqual(testScript.onSelectedChanged(event), "handled")
testScript.presentMessage.assert_not_called()
self.assertEqual(chromiumCalls, [(testScript, event)])
def test_selected_changed_defers_when_foreground_is_not_steam_game(self):
testScript = steam_script.Script.__new__(steam_script.Script)
source = object()
lastEvent = mock.Mock(event_string="Down", modifiers=0)
event = mock.Mock(
type="object:state-changed:selected",
source=source,
detail1=1,
any_data=None,
)
testScript.utilities = mock.Mock()
testScript.utilities.inDocumentContent.return_value = True
testScript.utilities.displayedText.return_value = ""
testScript.presentMessage = mock.Mock()
chromiumCalls = []
def chromiumOnSelectedChanged(self, selectedEvent):
chromiumCalls.append((self, selectedEvent))
return "handled"
with (
mock.patch.object(
steam_script.Script,
"_activeX11WindowIsSteamSelectionContext",
return_value=False,
),
mock.patch.object(steam_script.cthulhu_state, "lastNonModifierKeyEvent", lastEvent),
mock.patch.object(
steam_script.Chromium.Script,
"onSelectedChanged",
new=chromiumOnSelectedChanged,
),
):
self.assertEqual(testScript.onSelectedChanged(event), "handled")
testScript.presentMessage.assert_not_called()
self.assertEqual(chromiumCalls, [(testScript, event)])
def test_selection_changed_uses_selected_child_from_quick_access_container(self):
testScript = steam_script.Script.__new__(steam_script.Script)
source = object()
child = object()
lastEvent = mock.Mock(event_string=" ", modifiers=0)
event = mock.Mock(
type="object:selection-changed",
source=source,
detail1=0,
any_data=0,
)
testScript.utilities = mock.Mock()
testScript.utilities.inDocumentContent.return_value = True
testScript.utilities.displayedText.return_value = ""
testScript.presentMessage = mock.Mock()
testScript._lastSteamOverlaySelection = ("", 0.0)
with (
mock.patch.object(
steam_script.Script,
"_activeX11WindowIsSteamSelectionContext",
return_value=True,
),
mock.patch.object(steam_script.Script, "_hasSteamQuickAccessMarker", return_value=True),
mock.patch.object(steam_script.cthulhu_state, "lastNonModifierKeyEvent", lastEvent),
mock.patch.object(steam_script.cthulhu_state, "lastInputEvent", None),
mock.patch.object(steam_script.AXSelection, "get_selected_children", return_value=[child]),
mock.patch.object(steam_script.AXObject, "get_name", side_effect=lambda obj: "Friends" if obj is child else ""),
mock.patch.object(steam_script.AXUtilities, "is_page_tab", return_value=False),
mock.patch.object(steam_script.AXUtilities, "is_list_item", side_effect=lambda obj: obj is child),
mock.patch.object(steam_script.AXUtilities, "is_menu_item", return_value=False),
mock.patch.object(steam_script.AXUtilities, "is_button", return_value=False),
mock.patch.object(steam_script.AXUtilities, "is_push_button", return_value=False),
):
self.assertTrue(testScript.onSelectionChanged(event))
testScript.presentMessage.assert_called_once_with(
"Friends",
force=True,
interrupt=True,
)
def test_selected_changed_presents_steam_big_picture_tab_when_steam_is_foreground(self):
testScript = steam_script.Script.__new__(steam_script.Script)
source = object()
document = object()
lastEvent = mock.Mock(event_string="Right", modifiers=0)
event = mock.Mock(
type="object:state-changed:selected",
source=source,
detail1=1,
any_data=0,
)
testScript.utilities = mock.Mock()
testScript.utilities.inDocumentContent.return_value = True
testScript.utilities.displayedText.return_value = "ACTIVITY"
testScript.utilities.getTopLevelDocumentForObject.return_value = document
testScript.utilities.documentFrameURI.return_value = "https://steamloopback.host/index.html"
testScript.presentMessage = mock.Mock()
testScript._lastSteamOverlaySelection = ("", 0.0)
chromiumCalls = []
def chromiumOnSelectedChanged(self, selectedEvent):
chromiumCalls.append((self, selectedEvent))
return False
with (
mock.patch.object(
steam_script.Script,
"_activeX11WindowIsSteamSelectionContext",
return_value=True,
),
mock.patch.object(steam_script.Script, "_hasSteamQuickAccessMarker", return_value=False),
mock.patch.object(steam_script.cthulhu_state, "lastNonModifierKeyEvent", lastEvent),
mock.patch.object(steam_script.cthulhu_state, "lastInputEvent", None),
mock.patch.object(steam_script.AXObject, "get_name", return_value="ACTIVITY"),
mock.patch.object(steam_script.AXUtilities, "is_page_tab", return_value=True),
mock.patch.object(steam_script.AXUtilities, "is_list_item", return_value=False),
mock.patch.object(steam_script.AXUtilities, "is_menu_item", return_value=False),
mock.patch.object(steam_script.AXUtilities, "is_button", return_value=False),
mock.patch.object(steam_script.AXUtilities, "is_push_button", return_value=False),
mock.patch.object(
steam_script.Chromium.Script,
"onSelectedChanged",
new=chromiumOnSelectedChanged,
),
):
self.assertTrue(testScript.onSelectedChanged(event))
testScript.presentMessage.assert_called_once_with(
"ACTIVITY",
force=True,
interrupt=True,
)
self.assertEqual(chromiumCalls, [])
def test_selected_changed_defers_steam_tab_outside_steam_big_picture_document(self):
testScript = steam_script.Script.__new__(steam_script.Script)
source = object()
document = object()
lastEvent = mock.Mock(event_string="Right", modifiers=0)
event = mock.Mock(
type="object:state-changed:selected",
source=source,
detail1=1,
any_data=0,
)
testScript.utilities = mock.Mock()
testScript.utilities.inDocumentContent.return_value = True
testScript.utilities.displayedText.return_value = "ACTIVITY"
testScript.utilities.getTopLevelDocumentForObject.return_value = document
testScript.utilities.documentFrameURI.return_value = "https://store.steampowered.com/"
testScript.presentMessage = mock.Mock()
chromiumCalls = []
def chromiumOnSelectedChanged(self, selectedEvent):
chromiumCalls.append((self, selectedEvent))
return "handled"
with (
mock.patch.object(
steam_script.Script,
"_activeX11WindowIsSteamSelectionContext",
return_value=True,
),
mock.patch.object(steam_script.Script, "_hasSteamQuickAccessMarker", return_value=False),
mock.patch.object(steam_script.cthulhu_state, "lastNonModifierKeyEvent", lastEvent),
mock.patch.object(steam_script.cthulhu_state, "lastInputEvent", None),
mock.patch.object(steam_script.AXObject, "get_name", return_value="ACTIVITY"),
mock.patch.object(steam_script.AXUtilities, "is_page_tab", return_value=True),
mock.patch.object(steam_script.AXUtilities, "is_list_item", return_value=False),
mock.patch.object(steam_script.AXUtilities, "is_menu_item", return_value=False),
mock.patch.object(steam_script.AXUtilities, "is_button", return_value=False),
mock.patch.object(steam_script.AXUtilities, "is_push_button", return_value=False),
mock.patch.object(
steam_script.Chromium.Script,
"onSelectedChanged",
new=chromiumOnSelectedChanged,
),
):
self.assertEqual(testScript.onSelectedChanged(event), "handled")
testScript.presentMessage.assert_not_called()
self.assertEqual(chromiumCalls, [(testScript, event)])
def test_x11_steam_game_class_detects_steam_app_identifier(self):
classGroup = mock.Mock()
classGroup.get_name.return_value = "steam_app_1281930"
classGroup.get_res_class.return_value = "steam_app_1281930"
window = mock.Mock()
window.get_class_group_name.return_value = ""
window.get_class_instance_name.return_value = ""
window.get_class_group.return_value = classGroup
self.assertTrue(steam_script.Script._x11WindowHasSteamGameClass(window))
def test_x11_steam_game_class_rejects_plain_chromium_window(self):
classGroup = mock.Mock()
classGroup.get_name.return_value = "chromium"
classGroup.get_res_class.return_value = "Chromium"
window = mock.Mock()
window.get_class_group_name.return_value = "Chromium"
window.get_class_instance_name.return_value = "chromium"
window.get_class_group.return_value = classGroup
self.assertFalse(steam_script.Script._x11WindowHasSteamGameClass(window))
def test_x11_steam_big_picture_window_is_selection_context(self):
classGroup = mock.Mock()
classGroup.get_name.return_value = "steam"
classGroup.get_res_class.return_value = "steam"
window = mock.Mock()
window.get_name.return_value = "Steam Big Picture Mode"
window.get_class_group_name.return_value = "steam"
window.get_class_instance_name.return_value = "steamwebhelper"
window.get_class_group.return_value = classGroup
self.assertTrue(steam_script.Script._x11WindowIsSteamBigPicture(window))
def test_x11_plain_steam_window_is_not_big_picture_selection_context(self):
classGroup = mock.Mock()
classGroup.get_name.return_value = "steam"
classGroup.get_res_class.return_value = "steam"
window = mock.Mock()
window.get_name.return_value = "Steam"
window.get_class_group_name.return_value = "steam"
window.get_class_instance_name.return_value = "steamwebhelper"
window.get_class_group.return_value = classGroup
self.assertFalse(steam_script.Script._x11WindowIsSteamBigPicture(window))
class SteamReturnActivationTests(unittest.TestCase): class SteamReturnActivationTests(unittest.TestCase):
def test_return_activates_focused_steam_button(self): def test_return_activates_focused_steam_button(self):
testScript = steam_script.Script.__new__(steam_script.Script) testScript = steam_script.Script.__new__(steam_script.Script)
@@ -94,8 +497,283 @@ class SteamReturnActivationTests(unittest.TestCase):
performAction.assert_called_once_with(button) performAction.assert_called_once_with(button)
def test_return_activates_last_spoken_friends_panel_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._steamActiveQuickAccessTab = "Friends"
testScript._steamFriendsPanelActivationTarget = (
button,
"Username Playing Terraria",
100.0,
)
testScript._steamFriendsPanelActivationTargetMaxAge = 10.0
def has_action(obj, action_name):
return obj is button and action_name == "press"
with (
mock.patch.object(steam_script.time, "monotonic", return_value=105.0),
mock.patch.object(
steam_script.Script,
"_activeX11WindowIsSteamSelectionContext",
return_value=True,
),
mock.patch.object(steam_script.Script, "_hasSteamQuickAccessMarker", return_value=True),
mock.patch.object(testScript, "_getClickableActivationTarget", return_value=None),
mock.patch.object(steam_script.AXObject, "is_dead", return_value=False),
mock.patch.object(steam_script.AXObject, "find_ancestor_inclusive", return_value=None),
mock.patch.object(steam_script.AXUtilities, "is_page_tab", return_value=False),
mock.patch.object(steam_script.AXUtilities, "is_page_tab_list", return_value=False),
mock.patch.object(steam_script.AXUtilities, "is_list_item", 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_table_row", 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)
def test_return_defers_stale_friends_panel_activation_target(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._steamActiveQuickAccessTab = "Friends"
testScript._steamFriendsPanelActivationTarget = (
button,
"Username Playing Terraria",
100.0,
)
testScript._steamFriendsPanelActivationTargetMaxAge = 10.0
with (
mock.patch.object(steam_script.time, "monotonic", return_value=111.0),
mock.patch.object(steam_script.debug, "printMessage"),
mock.patch.object(
steam_script.Script,
"_activeX11WindowIsSteamSelectionContext",
return_value=True,
),
mock.patch.object(testScript, "_getClickableActivationTarget", return_value=None),
):
self.assertFalse(testScript.shouldConsumeKeyboardEvent(keyboardEvent, None))
self.assertEqual(testScript._steamFriendsPanelActivationTarget, (None, "", 0.0))
class SteamVirtualizedListMutationTests(unittest.TestCase): class SteamVirtualizedListMutationTests(unittest.TestCase):
def test_name_changed_presents_friends_panel_item_after_friends_tab_activation(self):
testScript = steam_script.Script.__new__(steam_script.Script)
row = object()
lastEvent = mock.Mock(event_string="Down", modifiers=0)
event = mock.Mock(
type="object:property-change:accessible-name",
source=row,
any_data="Username Playing Terraria",
detail1=0,
)
testScript.utilities = mock.Mock()
testScript.utilities.displayedText.side_effect = (
lambda obj: "Username Playing Terraria" if obj is row else ""
)
testScript.presentMessage = mock.Mock()
testScript._lastSteamOverlaySelection = ("", 0.0)
testScript._steamActiveQuickAccessTab = "Friends"
chromiumCalls = []
def chromiumOnNameChanged(self, nameEvent):
chromiumCalls.append((self, nameEvent))
return False
with (
mock.patch.object(
steam_script.Script,
"_activeX11WindowIsSteamSelectionContext",
return_value=True,
),
mock.patch.object(steam_script.Script, "_hasSteamQuickAccessMarker", return_value=True),
mock.patch.object(steam_script.cthulhu_state, "lastNonModifierKeyEvent", lastEvent),
mock.patch.object(steam_script.cthulhu_state, "lastInputEvent", None),
mock.patch.object(steam_script.AXUtilities, "is_page_tab", return_value=False),
mock.patch.object(steam_script.AXUtilities, "is_page_tab_list", return_value=False),
mock.patch.object(steam_script.AXUtilities, "is_list_item", 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_table_row", return_value=False),
mock.patch.object(steam_script.AXObject, "find_ancestor_inclusive", return_value=None),
mock.patch.object(
steam_script.Chromium.Script,
"onNameChanged",
new=chromiumOnNameChanged,
),
):
self.assertTrue(testScript.onNameChanged(event))
testScript.presentMessage.assert_called_once_with(
"Username Playing Terraria",
force=True,
interrupt=True,
)
self.assertEqual(
testScript._steamFriendsPanelActivationTarget[:2],
(row, "Username Playing Terraria"),
)
self.assertEqual(chromiumCalls, [])
def test_children_added_defers_friends_panel_load_artifact(self):
testScript = steam_script.Script.__new__(steam_script.Script)
source = object()
row = object()
lastEvent = mock.Mock(event_string="Down", modifiers=0)
event = mock.Mock(
type="object:children-changed:add",
source=source,
any_data=row,
detail1=0,
)
testScript.utilities = mock.Mock()
testScript.utilities.isSteamVirtualizedList.return_value = False
testScript.presentMessage = mock.Mock()
testScript._steamActiveQuickAccessTab = "Friends"
chromiumCalls = []
def chromiumOnChildrenAdded(self, addedEvent):
chromiumCalls.append((self, addedEvent))
return False
with (
mock.patch.object(steam_script.Script, "_isSteamNotification", return_value=False),
mock.patch.object(
steam_script.Script,
"_activeX11WindowIsSteamSelectionContext",
return_value=True,
),
mock.patch.object(steam_script.cthulhu_state, "lastNonModifierKeyEvent", lastEvent),
mock.patch.object(steam_script.cthulhu_state, "lastInputEvent", None),
mock.patch.object(
steam_script.Chromium.Script,
"onChildrenAdded",
new=chromiumOnChildrenAdded,
),
):
testScript.onChildrenAdded(event)
testScript.presentMessage.assert_not_called()
self.assertEqual(chromiumCalls, [(testScript, event)])
def test_name_changed_ignores_add_friend_panel_button(self):
testScript = steam_script.Script.__new__(steam_script.Script)
button = object()
lastEvent = mock.Mock(event_string="Down", modifiers=0)
event = mock.Mock(
type="object:property-change:accessible-name",
source=button,
any_data="Add Friend",
detail1=0,
)
testScript.utilities = mock.Mock()
testScript.utilities.displayedText.return_value = "Add Friend"
testScript.presentMessage = mock.Mock()
testScript._steamActiveQuickAccessTab = "Friends"
chromiumCalls = []
def chromiumOnNameChanged(self, nameEvent):
chromiumCalls.append((self, nameEvent))
return "handled"
with (
mock.patch.object(
steam_script.Script,
"_activeX11WindowIsSteamSelectionContext",
return_value=True,
),
mock.patch.object(steam_script.Script, "_hasSteamQuickAccessMarker", return_value=True),
mock.patch.object(steam_script.cthulhu_state, "lastNonModifierKeyEvent", lastEvent),
mock.patch.object(steam_script.cthulhu_state, "lastInputEvent", None),
mock.patch.object(steam_script.AXUtilities, "is_page_tab", return_value=False),
mock.patch.object(steam_script.AXUtilities, "is_page_tab_list", return_value=False),
mock.patch.object(steam_script.AXUtilities, "is_list_item", 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_table_row", return_value=False),
mock.patch.object(steam_script.AXObject, "find_ancestor_inclusive", return_value=None),
mock.patch.object(
steam_script.Chromium.Script,
"onNameChanged",
new=chromiumOnNameChanged,
),
):
self.assertEqual(testScript.onNameChanged(event), "handled")
testScript.presentMessage.assert_not_called()
self.assertEqual(chromiumCalls, [(testScript, event)])
def test_name_changed_ignores_friends_panel_chat_history_button(self):
testScript = steam_script.Script.__new__(steam_script.Script)
button = object()
lastEvent = mock.Mock(event_string="Down", modifiers=0)
event = mock.Mock(
type="object:property-change:accessible-name",
source=button,
any_data="LOADING OLDER MESSAGES... Username is typing a message...",
detail1=0,
)
testScript.utilities = mock.Mock()
testScript.utilities.displayedText.return_value = (
"LOADING OLDER MESSAGES... Username is typing a message..."
)
testScript.presentMessage = mock.Mock()
testScript._steamActiveQuickAccessTab = "Friends"
chromiumCalls = []
def chromiumOnNameChanged(self, nameEvent):
chromiumCalls.append((self, nameEvent))
return "handled"
with (
mock.patch.object(
steam_script.Script,
"_activeX11WindowIsSteamSelectionContext",
return_value=True,
),
mock.patch.object(steam_script.Script, "_hasSteamQuickAccessMarker", return_value=True),
mock.patch.object(steam_script.cthulhu_state, "lastNonModifierKeyEvent", lastEvent),
mock.patch.object(steam_script.cthulhu_state, "lastInputEvent", None),
mock.patch.object(steam_script.AXUtilities, "is_page_tab", return_value=False),
mock.patch.object(steam_script.AXUtilities, "is_page_tab_list", return_value=False),
mock.patch.object(steam_script.AXUtilities, "is_list_item", 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_table_row", return_value=False),
mock.patch.object(steam_script.AXObject, "find_ancestor_inclusive", return_value=None),
mock.patch.object(
steam_script.Chromium.Script,
"onNameChanged",
new=chromiumOnNameChanged,
),
):
self.assertEqual(testScript.onNameChanged(event), "handled")
testScript.presentMessage.assert_not_called()
self.assertEqual(chromiumCalls, [(testScript, event)])
def test_caret_moved_ignores_transient_loading_spinner(self): def test_caret_moved_ignores_transient_loading_spinner(self):
testScript = steam_script.Script.__new__(steam_script.Script) testScript = steam_script.Script.__new__(steam_script.Script)
source = object() source = object()