From 4776bf71532e9189d78dba98d3c6e914f26cda2c Mon Sep 17 00:00:00 2001 From: codeskyblue Date: Thu, 4 Apr 2024 21:41:37 +0800 Subject: [PATCH] add more tests --- .coveragerc | 12 ++-- Makefile | 6 +- docs/2to3.md | 7 ++- tests/unittests/test_xpath.py | 41 ++++++++++++- uiautomator2/ext/xpath/README.md | 3 - uiautomator2/ext/xpath/__init__.py | 11 ---- uiautomator2/xpath.py | 98 ++---------------------------- 7 files changed, 59 insertions(+), 119 deletions(-) delete mode 100644 uiautomator2/ext/xpath/README.md delete mode 100644 uiautomator2/ext/xpath/__init__.py diff --git a/.coveragerc b/.coveragerc index 91261e79..2db95efa 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,6 +1,10 @@ [run] branch = True +omit = + "tests/*" + "docs/*" + [report] ; Regexes for lines to exclude from consideration exclude_also = @@ -20,11 +24,3 @@ exclude_also = @(abc\.)?abstractmethod ignore_errors = True - -[paths] -source = - uiautomator2/ - -omit = - "tests/*" - "docs/*" diff --git a/Makefile b/Makefile index efe3b3c9..ddb95ef9 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,11 @@ test: poetry run pytest -v tests cov: - poetry run pytest -v tests/unittests --cov=. --cov-report xml --cov-report term + poetry run pytest -v tests/unittests \ + --cov-config=.coveragerc \ + --cov uiautomator2 \ + --cov-report xml \ + --cov-report term build: poetry self add "poetry-dynamic-versioning[plugin]" diff --git a/docs/2to3.md b/docs/2to3.md index ef1d8622..4a8a4603 100644 --- a/docs/2to3.md +++ b/docs/2to3.md @@ -24,7 +24,10 @@ enable_pretty_logging() - 移除 property u2.xpath.XPath.logger - 移除 property d.watcher.debug - 移除 d.settings["xpath_debug"] - - 移除 function XPath.apply_watch_from_yaml -- connect_usb函数中的init参数移除,这个参数目前没什么用了 + +- 移除 class AdbUI +- 移除 function XPath.apply_watch_from_yaml +- 移除 module uiautomator2.ext.xpath +- 移除 connect_usb函数中的init参数移除,这个参数目前没什么用了 TODO: atx-agent不在依赖外网下载,直接打包到内部 diff --git a/tests/unittests/test_xpath.py b/tests/unittests/test_xpath.py index 58583e60..07c23b0d 100644 --- a/tests/unittests/test_xpath.py +++ b/tests/unittests/test_xpath.py @@ -7,7 +7,7 @@ import pytest from unittest.mock import Mock from PIL import Image -from uiautomator2.xpath import XMLElement, XPathSelector, XPath, XPathElementNotFoundError +from uiautomator2.xpath import XMLElement, XPathSelector, XPath, XPathElementNotFoundError, is_xpath_syntax_ok mock = Mock() @@ -24,6 +24,28 @@ x = XPath(mock) + +def test_is_xpath_syntax_ok(): + assert is_xpath_syntax_ok("/a") + assert is_xpath_syntax_ok("//a") + assert is_xpath_syntax_ok("//a[@text='b]") is False + assert is_xpath_syntax_ok("//a[") is False + + +def test_xpath_selector(): + assert isinstance(x("n1"), XPathSelector) + assert isinstance(x("//TextView"), XPathSelector) + xp1 = x("n1") + xp2 = xp1.child("n2") + # xp1 should not be changed + assert xp1.get(timeout=0).text == "n1" + assert xp1.get_text() == "n1" + + # match return None or XMLElement + assert xp1.match() is not None + assert xp2.match() is None + + def test_xpath_click(): x("n1").click() assert mock.click.called @@ -73,11 +95,26 @@ def test_xpath_element(): el = x("n1").get(timeout=0) assert el.text == "n1" assert el.center() == (540, 50) + assert el.offset(0, 0) == (0, 0) + assert el.offset(1, 1) == (1080, 100) assert el.screenshot().size == (1080, 100) assert el.bounds == (0, 0, 1080, 100) - assert el.get_xpath() == "/hierarchy/FrameLayout/TextView[1]" + assert el.rect == (0, 0, 1080, 100) + assert isinstance(el.info, dict) + assert el.get_xpath(strip_index=True) == "/hierarchy/FrameLayout/TextView" mock.click.reset_mock() el.click() assert mock.click.called assert mock.click.call_args[0] == (540, 50) + + mock.long_click.reset_mock() + el.long_click() + assert mock.long_click.called + assert mock.long_click.call_args[0] == (540, 50) + + mock.swipe.reset_mock() + el.swipe("up") + assert mock.swipe.called + + diff --git a/uiautomator2/ext/xpath/README.md b/uiautomator2/ext/xpath/README.md deleted file mode 100644 index de9edfaf..00000000 --- a/uiautomator2/ext/xpath/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# uiautomator2 xpath extension - -Documents has moved to [../../XPATH.md](../../XPATH.md) diff --git a/uiautomator2/ext/xpath/__init__.py b/uiautomator2/ext/xpath/__init__.py deleted file mode 100644 index e412d77f..00000000 --- a/uiautomator2/ext/xpath/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -# coding: utf-8 -# - -import warnings - -from uiautomator2.xpath import * - -warnings.simplefilter("always", DeprecationWarning) - -warnings.warn("use \"import uiautomator2.xpath as xpath\" instead", - DeprecationWarning, 1) diff --git a/uiautomator2/xpath.py b/uiautomator2/xpath.py index 6f75c549..0c1a3619 100644 --- a/uiautomator2/xpath.py +++ b/uiautomator2/xpath.py @@ -4,16 +4,13 @@ from __future__ import absolute_import import functools -import io -import json import logging import re import threading import time from collections import defaultdict -from typing import Callable, List, Optional, Union +from typing import Any, Callable, Dict, List, Optional, Union -import adbutils from deprecated import deprecated from PIL import Image from lxml import etree @@ -41,7 +38,7 @@ def str2bytes(v: Union[str, bytes]) -> bytes: return v.encode("utf-8") -def is_xpath_syntax_ok(xpath_expression) -> bool: +def is_xpath_syntax_ok(xpath_expression: str) -> bool: try: etree.XPath(xpath_expression) return True # No error means the XPath syntax is likely okay @@ -465,7 +462,7 @@ def get(self, timeout=None): def get_last_match(self): return self.all(self._last_source)[0] - def get_text(self): + def get_text(self) -> Optional[str]: """ get element text @@ -475,7 +472,7 @@ def get_text(self): Raises: XPathElementNotFoundError """ - return self.get().attrib.get("text", "") + return self.get().text def set_text(self, text: str = ""): el = self.get() @@ -758,7 +755,7 @@ def attrib(self): return self.elem.attrib @property - def info(self): + def info(self) -> Dict[str, Any]: ret = {} for key in ( "text", @@ -781,87 +778,4 @@ def info(self): "resource-id" ) # this is better than resourceName ret["childCount"] = len(self.elem.getchildren()) - return ret - - -class AdbUI(DeviceInterface): - """ - Use adb command to run ui test - """ - - def __init__(self, d: adbutils.AdbDevice): - self._d = d - - def click(self, x, y): - self._d.click(x, y) - - def swipe(self, sx, sy, ex, ey, duration): - self._d.swipe(sx, sy, ex, ey, duration) - - def window_size(self): - w, h = self._d.window_size() - return w, h - - def dump_hierarchy(self): - return self._d.dump_hierarchy() - - def screenshot(self): - d = self._d - json_output = d.shell( - [ - "LD_LIBRARY_PATH=/data/local/tmp", - "/data/local/tmp/minicap", - "-i", - "2&>/dev/null", - ] - ).strip() - data = json.loads(json_output) - w, h, r = data["width"], data["height"], data["rotation"] - remote_image_path = "/sdcard/minicap.jpg" - d.shell(["rm", remote_image_path]) - d.shell( - [ - "LD_LIBRARY_PATH=/data/local/tmp", - "/data/local/tmp/minicap", - "-P", - "{0}x{1}@{0}x{1}/{2}".format(w, h, r), - "-s", - ">" + remote_image_path, - ] - ) # yapf: disable - - if d.sync.stat(remote_image_path).size == 0: - raise RuntimeError("screenshot using minicap error") - - buf = io.BytesIO() - for data in d.sync.iter_content(remote_image_path): - buf.write(data) - return Image.open(buf) - - -if __name__ == "__main__": - d = AdbUI(adbutils.adb.device()) - xpath = XPath(d) - # print(d.screenshot()) - # print(d.dump_hierarchy()[:20]) - xpath("App").click() - xpath("Alarm").click() - # init() - # import uiautomator2.ext.htmlreport as htmlreport - - # d = uiautomator2.connect() - # hrp = htmlreport.HTMLReport(d) - - # # take screenshot before each click - # hrp.patch_click() - # d.app_start("com.netease.cloudmusic", stop=True) - - # # watchers - # d.ext_xpath.when("跳过").click() - # # d.ext_xpath.when("知道了").click() - - # # steps - # d.ext_xpath("//*[@text='私人FM']/../android.widget.ImageView").click() - # d.ext_xpath("下一首").click() - # d.ext_xpath.sleep_watch(2) - # d.ext_xpath("转到上一层级").click() + return ret \ No newline at end of file