diff --git a/src/cthulhu/scripts/apps/steamwebhelper/script_utilities.py b/src/cthulhu/scripts/apps/steamwebhelper/script_utilities.py index a651c74..cfba0c8 100644 --- a/src/cthulhu/scripts/apps/steamwebhelper/script_utilities.py +++ b/src/cthulhu/scripts/apps/steamwebhelper/script_utilities.py @@ -44,6 +44,8 @@ class Utilities(ChromiumUtilities): def clearSteamVirtualizedListCaches(self) -> None: self.clearContentCache() self._steamInferredButtonLabels = {} + self._isUselessImage = {} + self._shouldFilter = {} def isSteamVirtualizedList(self, obj) -> bool: if not (obj and self.inDocumentContent(obj)): @@ -76,6 +78,21 @@ class Utilities(ChromiumUtilities): cache[obj] = inferredLabel return inferredLabel + def isUselessImage(self, obj) -> bool: + if not (obj and self.inDocumentContent(obj)): + return super().isUselessImage(obj) + + cached = self._isUselessImage.get(hash(obj)) + if cached is not None: + return cached + + rv = super().isUselessImage(obj) + if not rv: + rv = self._isRedundantSteamImage(obj) + + self._isUselessImage[hash(obj)] = rv + return rv + def _shouldInferSteamButtonLabel(self, obj) -> bool: if not (obj and self.inDocumentContent(obj)): return False @@ -113,6 +130,34 @@ class Utilities(ChromiumUtilities): return "Add Friend" return "" + def _isRedundantSteamImage(self, obj) -> bool: + if not AXUtilities.is_image_or_canvas(obj): + return False + + if AXObject.get_name(obj) or AXObject.get_description(obj): + return False + + if AXObject.get_child_count(obj): + return False + + if AXUtilities.is_focusable(obj): + return False + + if not AXObject.has_action(obj, "click-ancestor"): + return False + + roleDescription = self._normalizeSteamLabelText(AXObject.get_role_description(obj) or "") + if roleDescription and roleDescription.casefold() not in ["unlabeled image", "image"]: + return False + + nearbyLabel = self._getSteamNearbyImageLabel(obj) + if not self._isUsefulSteamLabel(nearbyLabel): + return False + + tokens = ["STEAM: Treating redundant image as useless:", obj, "(label:", nearbyLabel, ")"] + debug.printTokens(debug.LEVEL_INFO, tokens, True) + return True + def _getSteamNearbyButtonLabel(self, obj) -> str: parent = AXObject.get_parent(obj) if parent is None: @@ -132,6 +177,25 @@ class Utilities(ChromiumUtilities): return self._getSteamLabelFromChildren(grandParent, ignore=parent) + def _getSteamNearbyImageLabel(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: diff --git a/tests/test_steam_selection_regressions.py b/tests/test_steam_selection_regressions.py index e48df2a..4127375 100644 --- a/tests/test_steam_selection_regressions.py +++ b/tests/test_steam_selection_regressions.py @@ -267,5 +267,90 @@ class SteamLabelRecoveryTests(unittest.TestCase): self.assertEqual(utilities.displayedLabel(button), "Add Friend") +class SteamRedundantImageTests(unittest.TestCase): + def test_is_useless_image_treats_unlabeled_click_ancestor_image_with_nearby_text_as_useless(self): + testScript = mock.Mock(generatorCache={}) + utilities = steam_script_utilities.Utilities(testScript) + image = object() + parent = object() + entry = object() + + utilities.inDocumentContent = mock.Mock(return_value=True) + + def get_parent(obj): + if obj in (image, entry): + return parent + return None + + def get_name(obj): + if obj is entry: + return "Search for games or profiles..." + return "" + + def get_child_count(obj): + if obj is parent: + return 2 + return 0 + + def has_action(obj, actionName): + return obj is image and actionName == "click-ancestor" + + def iter_children(obj, pred=None): + children = [image, entry] if obj is parent else [] + 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, "isUselessImage", return_value=False), + mock.patch.object(steam_script_utilities.AXObject, "get_parent", side_effect=get_parent), + mock.patch.object(steam_script_utilities.AXObject, "get_name", side_effect=get_name), + mock.patch.object(steam_script_utilities.AXObject, "get_description", return_value=""), + mock.patch.object(steam_script_utilities.AXObject, "get_child_count", side_effect=get_child_count), + mock.patch.object(steam_script_utilities.AXObject, "has_action", side_effect=has_action), + mock.patch.object(steam_script_utilities.AXObject, "iter_children", side_effect=iter_children), + mock.patch.object(steam_script_utilities.AXObject, "supports_text", return_value=False), + mock.patch.object( + steam_script_utilities.AXObject, + "get_role_description", + side_effect=lambda obj: "Unlabeled image" if obj is image else "", + ), + mock.patch.object( + steam_script_utilities.AXUtilities, + "is_image_or_canvas", + side_effect=lambda obj: obj is image, + ), + mock.patch.object(steam_script_utilities.AXUtilities, "is_focusable", return_value=False), + mock.patch.object(steam_script_utilities.AXUtilities, "is_button", return_value=False), + mock.patch.object(steam_script_utilities.AXUtilities, "is_push_button", return_value=False), + ): + self.assertTrue(utilities.isUselessImage(image)) + + def test_is_useless_image_defers_to_generic_logic_without_nearby_text(self): + testScript = mock.Mock(generatorCache={}) + utilities = steam_script_utilities.Utilities(testScript) + image = object() + + utilities.inDocumentContent = mock.Mock(return_value=True) + + with ( + mock.patch.object(steam_script_utilities.ChromiumUtilities, "isUselessImage", return_value=False), + mock.patch.object(steam_script_utilities.AXObject, "get_parent", return_value=None), + mock.patch.object(steam_script_utilities.AXObject, "get_name", return_value=""), + mock.patch.object(steam_script_utilities.AXObject, "get_description", return_value=""), + mock.patch.object(steam_script_utilities.AXObject, "get_child_count", return_value=0), + mock.patch.object(steam_script_utilities.AXObject, "has_action", return_value=True), + mock.patch.object(steam_script_utilities.AXObject, "supports_text", return_value=False), + mock.patch.object(steam_script_utilities.AXObject, "get_role_description", return_value=""), + mock.patch.object( + steam_script_utilities.AXUtilities, + "is_image_or_canvas", + side_effect=lambda obj: obj is image, + ), + mock.patch.object(steam_script_utilities.AXUtilities, "is_focusable", return_value=False), + ): + self.assertFalse(utilities.isUselessImage(image)) + + if __name__ == "__main__": unittest.main()