Repository: openatx/uiautomator2

Issue:
(开头的xpath识别错误
使用括号开头的xpath`(//android.widget.ImageView)[2]`用来匹配第n个match的元素时
会被当作自定义的字符串匹配语法处理

- 手机型号：任意
- uiautomator2的版本号：2.16.25
- 手机截图：无
- 相关日志：`uiautomator2.exceptions.XPathElementNotFoundError: ["//*[@text='(//android.widget.ImageView)[2]' or @content-desc='(//android.widget.ImageView)[2]' or @resource-id='(//android.widget.ImageView)[2]']"]`
- 附上可能复现问题的代码：`d.xpath("(//android.widget.ImageView)[2]").get(1)`


Discussion:

Patch:
diff --git a/XPATH.md b/XPATH.md
index 2e80c27..32cef81 100644
--- a/XPATH.md
+++ b/XPATH.md
@@ -146,6 +146,29 @@ el.screenshot()
 # 控件滑动
 el.swipe("right") # left, right, up, down
 el.swipe("right", scale=0.9) # scale默认0.9, 意思是滑动距离为控件宽度的90%, 上滑则为高度的90%
+
+print(el.info)
+# output example
+{'index': '0',
+ 'text': '',
+ 'resourceId': 'com.example:id/home_searchedit',
+ 'checkable': 'true',
+ 'checked': 'true',
+ 'clickable': 'true',
+ 'enabled': 'true',
+ 'focusable': 'false',
+ 'focused': 'false',
+ 'scrollable': 'false',
+ 'longClickable': 'false',
+ 'password': 'false',
+ 'selected': 'false',
+ 'visibleToUser': 'true',
+ 'childCount': 0,
+ 'className': 'android.widget.Switch',
+ 'bounds': {'left': 882, 'top': 279, 'right': 1026, 'bottom': 423},
+ 'packageName': 'com.android.settings',
+ 'contentDescription': '',
+ 'resourceName': 'android:id/switch_widget'}
 ```
 
 ### 滑动到指定位置
diff --git a/uiautomator2/abcd.py b/uiautomator2/abcd.py
index 74090eb..22f10c7 100644
--- a/uiautomator2/abcd.py
+++ b/uiautomator2/abcd.py
@@ -5,7 +5,7 @@
 import abc
 
 
-class DeviceInterface(metaclass=abc.ABCMeta):
+class AbstractDevice(metaclass=abc.ABCMeta):
     @abc.abstractmethod
     def click(self, x: int, y: int):
         pass
diff --git a/uiautomator2/xpath.py b/uiautomator2/xpath.py
index 0c1a361..297532b 100644
--- a/uiautomator2/xpath.py
+++ b/uiautomator2/xpath.py
@@ -16,18 +16,18 @@ from PIL import Image
 from lxml import etree
 
 from uiautomator2._proto import Direction
-from uiautomator2.abcd import DeviceInterface
+from uiautomator2.abcd import AbstractDevice
 from uiautomator2.exceptions import XPathElementNotFoundError
 from uiautomator2.utils import inject_call, swipe_in_bounds
 
 
 logger = logging.getLogger(__name__)
 
-def safe_xmlstr(s):
+def safe_xmlstr(s: str) -> str:
     return s.replace("$", "-")
 
 
-def string_quote(s):
+def string_quote(s: str) -> str:
     """quick way to quote string"""
     return "{!r}".format(s)
 
@@ -46,11 +46,24 @@ def is_xpath_syntax_ok(xpath_expression: str) -> bool:
         return False  # Indicates a syntax error in the XPath expression
 
 
+def convert_to_camel_case(s: str) -> str:
+    """
+    Convert a string from kebab-case to camelCase.
+
+    :param s: A string in kebab-case format.
+    :return: A string converted to camelCase format.
+    """
+    parts = s.split('-')
+    # Convert the first letter of each part to uppercase, except for the first part
+    camel_case_str = parts[0] + ''.join(part.capitalize() for part in parts[1:])
+    return camel_case_str
+
+
 def strict_xpath(xpath: str) -> str:
     """make xpath to be computer recognized xpath"""
     orig_xpath = xpath
 
-    if xpath.startswith("/"):
+    if xpath.startswith("/") or xpath.startswith("(/"):
         pass
     elif xpath.startswith("@"):
         xpath = "//*[@resource-id={!r}]".format(xpath[1:])
@@ -96,7 +109,7 @@ class XPathError(Exception):
     """basic error for xpath plugin"""
 
 class XPath(object):
-    def __init__(self, d: DeviceInterface):
+    def __init__(self, d: AbstractDevice):
         """
         Args:
             d (uiautomator2 instance)
@@ -275,7 +288,7 @@ class XPath(object):
             direction = Direction.DOWN
         elif direction == Direction.HORIZ_FORWARD:  # Horizontal
             direction = Direction.LEFT
-        elif direction == Direction.HBACKWORD:
+        elif direction == Direction.HORIZ_BACKWARD:
             direction = Direction.RIGHT
 
         # FIXME(ssx): 还差一个检测是否到底的功能
@@ -757,25 +770,18 @@ class XMLElement(object):
     @property
     def info(self) -> Dict[str, Any]:
         ret = {}
-        for key in (
-            "text",
-            "focusable",
-            "enabled",
-            "focused",
-            "scrollable",
-            "selected",
-            "clickable",
-        ):
-            ret[key] = self.attrib.get(key)
+        for k, v in dict(self.attrib).items():
+            if k in ("bounds", "class", "package", "content-desc"):
+                continue
+            ret[convert_to_camel_case(k)] = v
+
+        ret["childCount"] = len(self.elem.getchildren())
         ret["className"] = self.elem.tag
         lx, ly, rx, ry = self.bounds
         ret["bounds"] = {"left": lx, "top": ly, "right": rx, "bottom": ry}
-        ret["contentDescription"] = self.attrib.get("content-desc")
-        ret["longClickable"] = self.attrib.get("long-clickable")
+
+        # 名字命名的有点奇怪，为了兼容性暂时保留
         ret["packageName"] = self.attrib.get("package")
+        ret["contentDescription"] = self.attrib.get("content-desc")
         ret["resourceName"] = self.attrib.get("resource-id")
-        ret["resourceId"] = self.attrib.get(
-            "resource-id"
-        )  # this is better than resourceName
-        ret["childCount"] = len(self.elem.getchildren())
         return ret
\ No newline at end of file


Test patch:
diff --git a/tests/test_xpath.py b/tests/test_xpath.py
index 07c23b0..5a14a61 100644
--- a/tests/test_xpath.py
+++ b/tests/test_xpath.py
@@ -44,7 +44,13 @@ def test_xpath_selector():
     # match return None or XMLElement
     assert xp1.match() is not None
     assert xp2.match() is None
-    
+
+
+def test_xpath_with_instance():
+    # issue: https://github.com/openatx/uiautomator2/issues/941
+    el = x('(//TextView)[2]').get(0)
+    assert el.text == "n2"
+
 
 def test_xpath_click():
     x("n1").click()