Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

remove xpath useless method #976

Merged
merged 5 commits into from
May 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 52 additions & 55 deletions XPATH.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ pip3 install -U uiautomator2
```

## 使用方法
目前该插件已经内置到uiautomator2中了,所以不需要plugin注册了。

### 简单用法

Expand Down Expand Up @@ -73,7 +72,7 @@ sl = d.xpath("@com.example:id/home_searchedit") # sl为XPathSelector对象

# 点击
sl.click()
sl.click(timeout=10) # 指定超时时间
sl.click(timeout=10) # 指定超时时间, 找不到抛出异常 XPathElementNotFoundError
sl.click_exists() # 存在即点击,返回是否点击成功
sl.click_exists(timeout=10) # 等待最多10s钟

Expand All @@ -94,7 +93,7 @@ el = sl.get(timeout=15)

# 修改默认的等待时间为15s
d.xpath.global_set("timeout", 15)
d.xpath.implicitly_wait(15) # 与上一行代码等价
d.xpath.implicitly_wait(15) # 与上一行代码等价 (TODO: Removed)

print(sl.exists) # 返回是否存在 (bool)
sl.get_last_match() # 获取上次匹配的XMLElement
Expand All @@ -111,15 +110,25 @@ for el in d.xpath('//android.widget.EditText').all():
print(el.elem) # 输出lxml解析出来的Node
print(el.text)

# 尚未测试的方法
# 点击位于控件包含坐标(50%, 50%)的方法
d.xpath("//*").position(0.5, 0.5).click()

# child操作
d.xpath('@android:id/list').child('/android.widget.TextView').click()
等价于 d.xpath('//*[@resource-id="android:id/list"]/android.widget.TextView').all()
```

高级查找语法

> Added in version 3.1

```python
# 查找 text=NFC AND id=android:id/item
(d.xpath("NFC") & d.xpath("@android:id/item")).get()

# 查找 text=NFC OR id=android:id/item
(d.xpath("NFC") | d.xpath("App") | d.xpath("Content")).get()

# 复杂一点也支持
((d.xpath("NFC") | d.xpath("@android:id/item")) & d.xpath("//android.widget.TextView")).get()

### `XMLElement`的操作

```python
Expand Down Expand Up @@ -246,6 +255,40 @@ def main():
print("还可以继续滚动")
```

### `PageSource`对象
> Added in version 3.1

这个属于高级用法,但是这个对象也最初级,几乎所有的函数都依赖它。

什么是PageSource?

PageSource是从d.dump_hierarchy()的返回值初始化来的。主要用于通过XPATH完成元素的查找工作。

用法?

```python
source = d.xpath.get_page_source()

# find_elements 是核心方法
elements = source.find_elements('//android.widget.TextView') # List[XMLElement]
for el in elements:
print(el.text)

# 获取坐标后点击
x, y = elements[0].center()
d.click(x, y)

# 多种条件的查询写法
es1 = source.find_elements('//android.widget.TextView')
es2 = source.find_elements(XPath('@android:id/content').joinpath("//*"))

# 寻找是TextView但不属于id=android:id/content下的节点
els = set(es1) - set(es2)

# 寻找是TextView同事属于id=android:id/content下的节点
els = set(es1) & set(es2)
```

## XPath规则
为了写起脚本来更快,我们自定义了一些简化的xpath规则

Expand Down Expand Up @@ -276,20 +319,14 @@ def main():

`%知道%` 匹配包含`知道`的文本,相当于 `//*[contains(text(), '知道')]`

**~~规则5~~(目前该功能已移除)**

> 另外来自Selenium PageObjects

`$知道` 匹配 通过`d.xpath.global_set("alias", dict)` dict字典中的内容, 如果不存在将使用`知道`来匹配

**规则 Last**

会匹配text 和 description字段

如 `搜索` 相当于 XPath `//*[@text="搜索" or @content-desc="搜索" or @resource-id="搜索"]`

## 特殊说明
- 有时className中包含有`$`字符,这个字符在XML中是不合法的,所以全部替换成了`-`
- 有时className中包含有`$@#&`字符,这个字符在XML中是不合法的,所以全部替换成了`.`

## XPath的一些高级用法
```
Expand Down Expand Up @@ -317,44 +354,4 @@ def main():
- [XPath的一些高级用法-简书](https://www.jianshu.com/p/4fef4142b33f)
- [XPath Quicksheet](https://devhints.io/xpath)

如有其他资料,欢迎提[Issues](https://github.com/openatx/uiautomator2/issues/new)补充

## 废弃功能
**别名定义** 从`1.3.4`版本不再支持别名

这种写法有点类似selenium中的[PageObjects](https://selenium-python.readthedocs.io/page-objects.html)

```python
# 这里是Python3的写法,python2的string定义需要改成 u"菜单" 注意前的这个u
d.xpath.global_set("alias", {
"菜单": "@com.netease.cloudmusic:id/qh", # TODO(ssx): maybe we can support P("@com.netease.cloudmusic:id/qh", wait_timeout=2) someday
"设置": "//android.widget.TextView[@text='设置']",
})

# 这里需要 $ 开头
d.xpath("$菜单").click() # 等价于 d.xpath()
d.xpath("$设置").click()


d.xpath("$菜单").click()
# 等价于 d.xpath("@com.netease.cloudmusic:id/qh").click()

d.xpath("$小吃").click() # 在这里会直接跑出XPathError异常,因为并不存在 小吃 这个alias

# alias_strict 设置项
d.xpath.global_set("alias_strict", False) # 默认 True
d.xpath("$小吃").click() # 这里就会正常运行
# 等价于
d.xpath('//*[@text="小吃" or @content-desc="小吃"]').click()
```

# 调整xpath的日志级别
目前默认logging.INFO

调整方法

```python
import logging

d.xpath.logger.setLevel(logging.DEBUG)
```
如有其他资料,欢迎提[Issues](https://github.com/openatx/uiautomator2/issues/new)补充
11 changes: 10 additions & 1 deletion docs/2to3.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
## New
- Add function enable_pretty_logging
- Add class AdbShellError, HierarchyEmptyError, HTTPError
- Add d.xpath.get_page_source() -> PageSource

## Breaking changes (移除的功能)

Expand Down Expand Up @@ -47,7 +48,7 @@

### Function removes
- Remove function current_app, use app_current instead
- Remove function XPath.apply_watch_from_yaml
- Remove function d.xpath.apply_watch_from_yaml
- Remove function healcheck, 原来是未来恢复uiautomator服务用的
- Remove function service(name: str) -> Service, 原本是用于做atx-agent的服务管理
- Remove function app_icon() -> Image, 该函数依赖atx-agent
Expand All @@ -56,6 +57,14 @@
- Remove function set_new_command_timeout(timeout: int), 用不着了
- Remove function open_identify(), 打开一个比较明显的界面,这个函数出了点毛病,先下掉了

XPath (d.xpath) methods
- remove dump_hierarchy
- remove get_last_hierarchy
- remove add_event_listener
- remove send_click, send_longclick, send_swipe, send_text, take_screenshot
- remove when, run_watchers, watch_background, watch_stop, watch_clear, sleep_watch
- remove position method, usage like d.xpath(...).position(0.2, 0.2)

### Command remove
- Remove "uiautomator2 healthcheck"
- Remove "uiautomator2 identify"
Expand Down
11 changes: 10 additions & 1 deletion mobile_tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,13 @@ def call(self):
assert 2 == a.n



def test_is_version_compatiable():
assert utils.is_version_compatiable("1.0.0", "1.0.0")
assert utils.is_version_compatiable("1.0.0", "1.0.1")
assert utils.is_version_compatiable("1.0.0", "1.2.0")
assert utils.is_version_compatiable("1.0.1", "1.1.0")

assert not utils.is_version_compatiable("1.0.1", "2.1.0")
assert not utils.is_version_compatiable("1.3.1", "1.3.0")
assert not utils.is_version_compatiable("1.3.1", "1.2.0")
assert not utils.is_version_compatiable("1.3.1", "1.2.2")
20 changes: 3 additions & 17 deletions mobile_tests/test_xpath.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,6 @@
import uiautomator2 as u2


def test_xpath_selector(dev: u2.Device):
sel1 = dev.xpath("/a")
print(str(sel1), type(str(sel1)))
assert str(sel1).endswith("=/a")
assert str(sel1.child("/b")).endswith("=/a/b")
assert str(sel1).endswith("=/a") # sel1 should not be changed
assert str(sel1.xpath("/b")).endswith("=/a|/b")
assert str(sel1.xpath(["/b", "/c"])).endswith("=/a|/b|/c")
assert sel1.position(0.1, 0.1) != sel1
assert sel1.fallback("click") != sel1
with pytest.raises(ValueError):
sel1.fallback("invalid-action")


def test_get_text(dev: u2.Device):
assert dev.xpath("App").get_text() == "App"

Expand Down Expand Up @@ -55,15 +41,15 @@ def test_element_all(dev: u2.Device):


def test_watcher(dev: u2.Device, request):
dev.xpath.when("App").click()
dev.xpath.watch_background(interval=1.0)
dev.watcher.when("App").click()
dev.watcher.start(interval=1.0)

event = threading.Event()

def _set_event(e):
e.set()

dev.xpath.when("Action Bar").call(partial(_set_event, event))
dev.watcher.when("Action Bar").call(partial(_set_event, event))
assert event.wait(5.0), "xpath not trigger callback"


Expand Down
40 changes: 38 additions & 2 deletions tests/test_xpath.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import pytest
from unittest.mock import Mock
from PIL import Image
from uiautomator2.xpath import XMLElement, XPathSelector, XPath, XPathElementNotFoundError, is_xpath_syntax_ok
from uiautomator2.xpath import XMLElement, XPath, XPathSelector, XPathEntry, XPathElementNotFoundError, convert_to_camel_case, is_xpath_syntax_ok, safe_xmlstr, str2bytes, strict_xpath


mock = Mock()
Expand All @@ -22,7 +22,21 @@
</hierarchy>
"""

x = XPath(mock)
x = XPathEntry(mock)


def test_safe_xmlstr():
for input, expect in [
('android.widget.TextView', 'android.widget.TextView'),
('test$123', 'test.123'),
('$@#&123.456$', '123.456'),
]:
assert safe_xmlstr(input) == expect


def test_str2bytes():
assert str2bytes(b'123') == b'123'
assert str2bytes('123') == b'123'


def test_is_xpath_syntax_ok():
Expand All @@ -32,6 +46,28 @@ def test_is_xpath_syntax_ok():
assert is_xpath_syntax_ok("//a[") is False


def test_convert_to_camel_case():
assert convert_to_camel_case("hello-world") == "helloWorld"


def test_strict_xpath():
for (input, expect) in [
("@n1", "//*[@resource-id='n1']"),
("//TextView", "//TextView"),
("//TextView[@text='n1']", "//TextView[@text='n1']"),
("(//TextView)[2]", "(//TextView)[2]"),
("//TextView/", "//TextView"), # test rstrip /
]:
assert strict_xpath(input) == expect


def test_XPath():
xp = XPath("//TextView")
assert xp == "//TextView"
assert xp.joinpath("/n1") == "//TextView/n1"



def test_xpath_selector():
assert isinstance(x("n1"), XPathSelector)
assert isinstance(x("//TextView"), XPathSelector)
Expand Down
20 changes: 12 additions & 8 deletions uiautomator2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
from uiautomator2 import xpath
from uiautomator2._proto import HTTP_TIMEOUT, SCROLL_STEPS, Direction
from uiautomator2._selector import Selector, UiObject
from uiautomator2.exceptions import AdbShellError, BaseException, DeviceError, HierarchyEmptyError, SessionBrokenError
from uiautomator2.exceptions import AdbShellError, BaseException, ConnectError, DeviceError, HierarchyEmptyError, SessionBrokenError
from uiautomator2.settings import Settings
from uiautomator2.swipe import SwipeExt
from uiautomator2.utils import list2cmdline
Expand Down Expand Up @@ -71,7 +71,10 @@
wait for device came online, if device is remote, reconnect every 1s

Returns:
adbutils.AdbDevice or None
adbutils.AdbDevice

Raises:
ConnectError
"""
for d in adbutils.adb.device_list():
if d.serial == self._serial:
Expand All @@ -98,7 +101,7 @@
except (adbutils.AdbError, adbutils.AdbTimeout):
continue
return adb.device(self._serial)
return None
raise ConnectError(f"device {self._serial} not online")

Check warning on line 104 in uiautomator2/__init__.py

View check run for this annotation

Codecov / codecov/patch

uiautomator2/__init__.py#L104

Added line #L104 was not covered by tests

@property
def adb_device(self) -> adbutils.AdbDevice:
Expand Down Expand Up @@ -406,12 +409,13 @@

def long_click(self, x, y, duration: float = .5):
'''long click at arbitrary coordinates.

Args:
duration (float): seconds of pressed
'''
x, y = self.pos_rel2abs(x, y)
with self._operation_delay("click"):
return self.touch.down(x, y).sleep(duration).up(x, y)
self.jsonrpc.click(x, y, int(duration*1000))

Check warning on line 418 in uiautomator2/__init__.py

View check run for this annotation

Codecov / codecov/patch

uiautomator2/__init__.py#L418

Added line #L418 was not covered by tests

def swipe(self, fx, fy, tx, ty, duration: Optional[float] = None, steps: Optional[int] = None):
"""
Expand Down Expand Up @@ -901,9 +905,9 @@
return self.jsonrpc.makeToast(text, duration * 1000)

def unlock(self):
""" unlock screen """
""" unlock screen with swipe from left-bottom to right-top """
if not self.info['screenOn']:
self.press("power")
self.shell("input keyevent WAKEUP")

Check warning on line 910 in uiautomator2/__init__.py

View check run for this annotation

Codecov / codecov/patch

uiautomator2/__init__.py#L910

Added line #L910 was not covered by tests
self.swipe(0.1, 0.9, 0.9, 0.1)


Expand Down Expand Up @@ -1033,8 +1037,8 @@
return Watcher(self)

@cached_property
def xpath(self) -> xpath.XPath:
return xpath.XPath(self)
def xpath(self) -> xpath.XPathEntry:
return xpath.XPathEntry(self)

Check warning on line 1041 in uiautomator2/__init__.py

View check run for this annotation

Codecov / codecov/patch

uiautomator2/__init__.py#L1041

Added line #L1041 was not covered by tests

@cached_property
def image(self):
Expand Down
Loading